blob: 31a1c44cb8a489dee7b6ea91b7453557b56bf043 [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 Gregorio9086bd32013-06-14 16:32:05 -040029import logging
Joe Gregoriod0bd3882011-11-22 09:49:47 -050030import mimeparse
31import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050032import os
Joe Gregorio9086bd32013-06-14 16:32:05 -040033import random
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040034import sys
Joe Gregorio9086bd32013-06-14 16:32:05 -040035import time
Joe Gregorio66f57522011-11-30 11:00:00 -050036import urllib
37import urlparse
38import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050039
Joe Gregorio654f4a22012-02-09 14:15:44 -050040from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050041from email.mime.multipart import MIMEMultipart
42from email.mime.nonmultipart import MIMENonMultipart
43from email.parser import FeedParser
44from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050045from errors import HttpError
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040046from errors import InvalidChunkSizeError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050047from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040048from errors import UnexpectedBodyError
49from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050050from model import JsonModel
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040051from oauth2client import util
Joe Gregorio549230c2012-01-11 10:38:05 -050052from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040053
54
Joe Gregorio910b9b12012-06-12 09:36:30 -040055DEFAULT_CHUNK_SIZE = 512*1024
56
Joe Gregorio2728ed12012-11-16 15:48:26 -050057MAX_URI_LENGTH = 2048
Joe Gregorioba5c7902012-08-03 12:48:16 -040058
Joe Gregorio910b9b12012-06-12 09:36:30 -040059
Joe Gregoriod0bd3882011-11-22 09:49:47 -050060class MediaUploadProgress(object):
61 """Status of a resumable upload."""
62
63 def __init__(self, resumable_progress, total_size):
64 """Constructor.
65
66 Args:
67 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040068 total_size: int, total bytes in complete upload, or None if the total
69 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050070 """
71 self.resumable_progress = resumable_progress
72 self.total_size = total_size
73
74 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040075 """Percent of upload completed, as a float.
76
77 Returns:
78 the percentage complete as a float, returning 0.0 if the total size of
79 the upload is unknown.
80 """
81 if self.total_size is not None:
82 return float(self.resumable_progress) / float(self.total_size)
83 else:
84 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050085
86
Joe Gregorio708388c2012-06-15 13:43:04 -040087class MediaDownloadProgress(object):
88 """Status of a resumable download."""
89
90 def __init__(self, resumable_progress, total_size):
91 """Constructor.
92
93 Args:
94 resumable_progress: int, bytes received so far.
95 total_size: int, total bytes in complete download.
96 """
97 self.resumable_progress = resumable_progress
98 self.total_size = total_size
99
100 def progress(self):
101 """Percent of download completed, as a float.
102
103 Returns:
104 the percentage complete as a float, returning 0.0 if the total size of
105 the download is unknown.
106 """
107 if self.total_size is not None:
108 return float(self.resumable_progress) / float(self.total_size)
109 else:
110 return 0.0
111
112
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500113class MediaUpload(object):
114 """Describes a media object to upload.
115
116 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400117
118 Note that subclasses of MediaUpload may allow you to control the chunksize
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400119 when uploading a media object. It is important to keep the size of the chunk
120 as large as possible to keep the upload efficient. Other factors may influence
Joe Gregorio88f699f2012-06-07 13:36:06 -0400121 the size of the chunk you use, particularly if you are working in an
122 environment where individual HTTP requests may have a hardcoded time limit,
123 such as under certain classes of requests under Google App Engine.
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400124
125 Streams are io.Base compatible objects that support seek(). Some MediaUpload
126 subclasses support using streams directly to upload data. Support for
127 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
128 platform that stream will be used for uploading the media object. The support
129 for streaming is indicated by has_stream() returning True. The stream() method
130 should return an io.Base object that supports seek(). On platforms where the
131 underlying httplib module supports streaming, for example Python 2.6 and
132 later, the stream will be passed into the http library which will result in
133 less memory being used and possibly faster uploads.
134
135 If you need to upload media that can't be uploaded using any of the existing
136 MediaUpload sub-class then you can sub-class MediaUpload for your particular
137 needs.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500138 """
139
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500140 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400141 """Chunk size for resumable uploads.
142
143 Returns:
144 Chunk size in bytes.
145 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500146 raise NotImplementedError()
147
148 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400149 """Mime type of the body.
150
151 Returns:
152 Mime type.
153 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500154 return 'application/octet-stream'
155
Joe Gregorio910b9b12012-06-12 09:36:30 -0400156 def size(self):
157 """Size of upload.
158
159 Returns:
160 Size of the body, or None of the size is unknown.
161 """
162 return None
163
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500164 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400165 """Whether this upload is resumable.
166
167 Returns:
168 True if resumable upload or False.
169 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500170 return False
171
Joe Gregorio910b9b12012-06-12 09:36:30 -0400172 def getbytes(self, begin, end):
173 """Get bytes from the media.
174
175 Args:
176 begin: int, offset from beginning of file.
177 length: int, number of bytes to read, starting at begin.
178
179 Returns:
180 A string of bytes read. May be shorter than length if EOF was reached
181 first.
182 """
183 raise NotImplementedError()
184
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400185 def has_stream(self):
186 """Does the underlying upload support a streaming interface.
187
188 Streaming means it is an io.IOBase subclass that supports seek, i.e.
189 seekable() returns True.
190
191 Returns:
192 True if the call to stream() will return an instance of a seekable io.Base
193 subclass.
194 """
195 return False
196
197 def stream(self):
198 """A stream interface to the data being uploaded.
199
200 Returns:
201 The returned value is an io.IOBase subclass that supports seek, i.e.
202 seekable() returns True.
203 """
204 raise NotImplementedError()
205
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400206 @util.positional(1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500207 def _to_json(self, strip=None):
208 """Utility function for creating a JSON representation of a MediaUpload.
209
210 Args:
211 strip: array, An array of names of members to not include in the JSON.
212
213 Returns:
214 string, a JSON representation of this instance, suitable to pass to
215 from_json().
216 """
217 t = type(self)
218 d = copy.copy(self.__dict__)
219 if strip is not None:
220 for member in strip:
221 del d[member]
222 d['_class'] = t.__name__
223 d['_module'] = t.__module__
224 return simplejson.dumps(d)
225
226 def to_json(self):
227 """Create a JSON representation of an instance of MediaUpload.
228
229 Returns:
230 string, a JSON representation of this instance, suitable to pass to
231 from_json().
232 """
233 return self._to_json()
234
235 @classmethod
236 def new_from_json(cls, s):
237 """Utility class method to instantiate a MediaUpload subclass from a JSON
238 representation produced by to_json().
239
240 Args:
241 s: string, JSON from to_json().
242
243 Returns:
244 An instance of the subclass of MediaUpload that was serialized with
245 to_json().
246 """
247 data = simplejson.loads(s)
248 # Find and call the right classmethod from_json() to restore the object.
249 module = data['_module']
250 m = __import__(module, fromlist=module.split('.')[:-1])
251 kls = getattr(m, data['_class'])
252 from_json = getattr(kls, 'from_json')
253 return from_json(s)
254
Joe Gregorio66f57522011-11-30 11:00:00 -0500255
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400256class MediaIoBaseUpload(MediaUpload):
257 """A MediaUpload for a io.Base objects.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500258
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400259 Note that the Python file object is compatible with io.Base and can be used
260 with this class also.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500261
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400262 fh = io.BytesIO('...Some data to upload...')
263 media = MediaIoBaseUpload(fh, mimetype='image/png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400264 chunksize=1024*1024, resumable=True)
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400265 farm.animals().insert(
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400266 id='cow',
267 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500268 media_body=media).execute()
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400269
270 Depending on the platform you are working on, you may pass -1 as the
271 chunksize, which indicates that the entire file should be uploaded in a single
272 request. If the underlying platform supports streams, such as Python 2.6 or
273 later, then this can be very efficient as it avoids multiple connections, and
274 also avoids loading the entire file into memory before sending it. Note that
275 Google App Engine has a 5MB limit on request size, so you should never set
276 your chunksize larger than 5MB, or to -1.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500277 """
278
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400279 @util.positional(3)
280 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
281 resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500282 """Constructor.
283
284 Args:
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400285 fd: io.Base or file object, The source of the bytes to upload. MUST be
286 opened in blocking mode, do not use streams opened in non-blocking mode.
287 The given stream must be seekable, that is, it must be able to call
288 seek() on fd.
289 mimetype: string, Mime-type of the file.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500290 chunksize: int, File will be uploaded in chunks of this many bytes. Only
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400291 used if resumable=True. Pass in a value of -1 if the file is to be
292 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
293 on request size, so you should never set your chunksize larger than 5MB,
294 or to -1.
Joe Gregorio66f57522011-11-30 11:00:00 -0500295 resumable: bool, True if this is a resumable upload. False means upload
296 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500297 """
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400298 super(MediaIoBaseUpload, self).__init__()
299 self._fd = fd
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500300 self._mimetype = mimetype
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400301 if not (chunksize == -1 or chunksize > 0):
302 raise InvalidChunkSizeError()
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500303 self._chunksize = chunksize
304 self._resumable = resumable
305
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400306 self._fd.seek(0, os.SEEK_END)
307 self._size = self._fd.tell()
308
Joe Gregorio910b9b12012-06-12 09:36:30 -0400309 def chunksize(self):
310 """Chunk size for resumable uploads.
311
312 Returns:
313 Chunk size in bytes.
314 """
315 return self._chunksize
316
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500317 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400318 """Mime type of the body.
319
320 Returns:
321 Mime type.
322 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500323 return self._mimetype
324
325 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400326 """Size of upload.
327
328 Returns:
329 Size of the body, or None of the size is unknown.
330 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500331 return self._size
332
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500333 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400334 """Whether this upload is resumable.
335
336 Returns:
337 True if resumable upload or False.
338 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500339 return self._resumable
340
341 def getbytes(self, begin, length):
342 """Get bytes from the media.
343
344 Args:
345 begin: int, offset from beginning of file.
346 length: int, number of bytes to read, starting at begin.
347
348 Returns:
349 A string of bytes read. May be shorted than length if EOF was reached
350 first.
351 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500352 self._fd.seek(begin)
353 return self._fd.read(length)
354
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400355 def has_stream(self):
356 """Does the underlying upload support a streaming interface.
357
358 Streaming means it is an io.IOBase subclass that supports seek, i.e.
359 seekable() returns True.
360
361 Returns:
362 True if the call to stream() will return an instance of a seekable io.Base
363 subclass.
364 """
365 return True
366
367 def stream(self):
368 """A stream interface to the data being uploaded.
369
370 Returns:
371 The returned value is an io.IOBase subclass that supports seek, i.e.
372 seekable() returns True.
373 """
374 return self._fd
375
376 def to_json(self):
377 """This upload type is not serializable."""
378 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
379
380
381class MediaFileUpload(MediaIoBaseUpload):
382 """A MediaUpload for a file.
383
384 Construct a MediaFileUpload and pass as the media_body parameter of the
385 method. For example, if we had a service that allowed uploading images:
386
387
388 media = MediaFileUpload('cow.png', mimetype='image/png',
389 chunksize=1024*1024, resumable=True)
390 farm.animals().insert(
391 id='cow',
392 name='cow.png',
393 media_body=media).execute()
394
395 Depending on the platform you are working on, you may pass -1 as the
396 chunksize, which indicates that the entire file should be uploaded in a single
397 request. If the underlying platform supports streams, such as Python 2.6 or
398 later, then this can be very efficient as it avoids multiple connections, and
399 also avoids loading the entire file into memory before sending it. Note that
400 Google App Engine has a 5MB limit on request size, so you should never set
401 your chunksize larger than 5MB, or to -1.
402 """
403
404 @util.positional(2)
405 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
406 resumable=False):
407 """Constructor.
408
409 Args:
410 filename: string, Name of the file.
411 mimetype: string, Mime-type of the file. If None then a mime-type will be
412 guessed from the file extension.
413 chunksize: int, File will be uploaded in chunks of this many bytes. Only
414 used if resumable=True. Pass in a value of -1 if the file is to be
415 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
416 on request size, so you should never set your chunksize larger than 5MB,
417 or to -1.
418 resumable: bool, True if this is a resumable upload. False means upload
419 in a single request.
420 """
421 self._filename = filename
422 fd = open(self._filename, 'rb')
423 if mimetype is None:
424 (mimetype, encoding) = mimetypes.guess_type(filename)
425 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
426 resumable=resumable)
427
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500428 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400429 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500430
431 Returns:
432 string, a JSON representation of this instance, suitable to pass to
433 from_json().
434 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400435 return self._to_json(strip=['_fd'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500436
437 @staticmethod
438 def from_json(s):
439 d = simplejson.loads(s)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400440 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
441 chunksize=d['_chunksize'], resumable=d['_resumable'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500442
443
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400444class MediaInMemoryUpload(MediaIoBaseUpload):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500445 """MediaUpload for a chunk of bytes.
446
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400447 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
448 the stream.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500449 """
450
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400451 @util.positional(2)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500452 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400453 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400454 """Create a new MediaInMemoryUpload.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500455
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400456 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
457 the stream.
458
459 Args:
460 body: string, Bytes of body content.
461 mimetype: string, Mime-type of the file or default of
462 'application/octet-stream'.
463 chunksize: int, File will be uploaded in chunks of this many bytes. Only
464 used if resumable=True.
465 resumable: bool, True if this is a resumable upload. False means upload
466 in a single request.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500467 """
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400468 fd = StringIO.StringIO(body)
469 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
470 resumable=resumable)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500471
472
Joe Gregorio708388c2012-06-15 13:43:04 -0400473class MediaIoBaseDownload(object):
474 """"Download media resources.
475
476 Note that the Python file object is compatible with io.Base and can be used
477 with this class also.
478
479
480 Example:
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400481 request = farms.animals().get_media(id='cow')
482 fh = io.FileIO('cow.png', mode='wb')
Joe Gregorio708388c2012-06-15 13:43:04 -0400483 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
484
485 done = False
486 while done is False:
487 status, done = downloader.next_chunk()
488 if status:
489 print "Download %d%%." % int(status.progress() * 100)
490 print "Download Complete!"
491 """
492
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400493 @util.positional(3)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400494 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
Joe Gregorio708388c2012-06-15 13:43:04 -0400495 """Constructor.
496
497 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400498 fd: io.Base or file object, The stream in which to write the downloaded
Joe Gregorio708388c2012-06-15 13:43:04 -0400499 bytes.
500 request: apiclient.http.HttpRequest, the media request to perform in
501 chunks.
502 chunksize: int, File will be downloaded in chunks of this many bytes.
503 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400504 self._fd = fd
505 self._request = request
506 self._uri = request.uri
507 self._chunksize = chunksize
508 self._progress = 0
509 self._total_size = None
510 self._done = False
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400511 self._original_follow_redirects = request.http.follow_redirects
512 request.http.follow_redirects = False
Joe Gregorio708388c2012-06-15 13:43:04 -0400513
Joe Gregorio9086bd32013-06-14 16:32:05 -0400514 # Stubs for testing.
515 self._sleep = time.sleep
516 self._rand = random.random
517
518 @util.positional(1)
519 def next_chunk(self, num_retries=0):
Joe Gregorio708388c2012-06-15 13:43:04 -0400520 """Get the next chunk of the download.
521
Joe Gregorio9086bd32013-06-14 16:32:05 -0400522 Args:
523 num_retries: Integer, number of times to retry 500's with randomized
524 exponential backoff. If all retries fail, the raised HttpError
525 represents the last request. If zero (default), we attempt the
526 request only once.
527
Joe Gregorio708388c2012-06-15 13:43:04 -0400528 Returns:
529 (status, done): (MediaDownloadStatus, boolean)
530 The value of 'done' will be True when the media has been fully
531 downloaded.
532
533 Raises:
534 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400535 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio708388c2012-06-15 13:43:04 -0400536 """
537 headers = {
538 'range': 'bytes=%d-%d' % (
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400539 self._progress, self._progress + self._chunksize)
Joe Gregorio708388c2012-06-15 13:43:04 -0400540 }
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400541 http = self._request.http
Joe Gregorio708388c2012-06-15 13:43:04 -0400542
Joe Gregorio9086bd32013-06-14 16:32:05 -0400543 for retry_num in xrange(num_retries + 1):
544 if retry_num > 0:
545 self._sleep(self._rand() * 2**retry_num)
546 logging.warning(
547 'Retry #%d for media download: GET %s, following status: %d'
548 % (retry_num, self._uri, resp.status))
549
550 resp, content = http.request(self._uri, headers=headers)
551 if resp.status < 500:
552 break
553
Joe Gregorio708388c2012-06-15 13:43:04 -0400554 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400555 self._uri = resp['location']
556 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400557 if resp.status in [200, 206]:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400558 self._progress += len(content)
559 self._fd.write(content)
Joe Gregorio708388c2012-06-15 13:43:04 -0400560
561 if 'content-range' in resp:
562 content_range = resp['content-range']
563 length = content_range.rsplit('/', 1)[1]
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400564 self._total_size = int(length)
Joe Gregorio708388c2012-06-15 13:43:04 -0400565
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400566 if self._progress == self._total_size:
567 self._done = True
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400568 self._request.http.follow_redirects = self._original_follow_redirects
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400569 return MediaDownloadProgress(self._progress, self._total_size), self._done
Joe Gregorio708388c2012-06-15 13:43:04 -0400570 else:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400571 raise HttpError(resp, content, uri=self._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400572
573
Joe Gregorio5c120db2012-08-23 09:13:55 -0400574class _StreamSlice(object):
575 """Truncated stream.
576
577 Takes a stream and presents a stream that is a slice of the original stream.
578 This is used when uploading media in chunks. In later versions of Python a
579 stream can be passed to httplib in place of the string of data to send. The
580 problem is that httplib just blindly reads to the end of the stream. This
581 wrapper presents a virtual stream that only reads to the end of the chunk.
582 """
583
584 def __init__(self, stream, begin, chunksize):
585 """Constructor.
586
587 Args:
588 stream: (io.Base, file object), the stream to wrap.
589 begin: int, the seek position the chunk begins at.
590 chunksize: int, the size of the chunk.
591 """
592 self._stream = stream
593 self._begin = begin
594 self._chunksize = chunksize
595 self._stream.seek(begin)
596
597 def read(self, n=-1):
598 """Read n bytes.
599
600 Args:
601 n, int, the number of bytes to read.
602
603 Returns:
604 A string of length 'n', or less if EOF is reached.
605 """
606 # The data left available to read sits in [cur, end)
607 cur = self._stream.tell()
608 end = self._begin + self._chunksize
609 if n == -1 or cur + n > end:
610 n = end - cur
611 return self._stream.read(n)
612
613
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400614class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500615 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400616
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400617 @util.positional(4)
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500618 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500619 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500620 body=None,
621 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500622 methodId=None,
623 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500624 """Constructor for an HttpRequest.
625
Joe Gregorioaf276d22010-12-09 14:26:58 -0500626 Args:
627 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500628 postproc: callable, called on the HTTP response and content to transform
629 it into a data object before returning, or raising an exception
630 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500631 uri: string, the absolute URI to send the request to
632 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500633 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500634 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500635 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500636 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500637 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400638 self.uri = uri
639 self.method = method
640 self.body = body
641 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500642 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400643 self.http = http
644 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500645 self.resumable = resumable
Ali Afshar164f37e2013-01-07 14:05:45 -0800646 self.response_callbacks = []
Joe Gregorio910b9b12012-06-12 09:36:30 -0400647 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500648
Joe Gregorio66f57522011-11-30 11:00:00 -0500649 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500650 major, minor, params = mimeparse.parse_mime_type(
651 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500652
Joe Gregorio945be3e2012-01-27 17:01:06 -0500653 # The size of the non-media part of the request.
654 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500655
656 # The resumable URI to send chunks to.
657 self.resumable_uri = None
658
659 # The bytes that have been uploaded.
660 self.resumable_progress = 0
661
Joe Gregorio9086bd32013-06-14 16:32:05 -0400662 # Stubs for testing.
663 self._rand = random.random
664 self._sleep = time.sleep
665
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400666 @util.positional(1)
Joe Gregorio9086bd32013-06-14 16:32:05 -0400667 def execute(self, http=None, num_retries=0):
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400668 """Execute the request.
669
Joe Gregorioaf276d22010-12-09 14:26:58 -0500670 Args:
671 http: httplib2.Http, an http object to be used in place of the
672 one the HttpRequest request object was constructed with.
Joe Gregorio9086bd32013-06-14 16:32:05 -0400673 num_retries: Integer, number of times to retry 500's with randomized
674 exponential backoff. If all retries fail, the raised HttpError
675 represents the last request. If zero (default), we attempt the
676 request only once.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500677
678 Returns:
679 A deserialized object model of the response body as determined
680 by the postproc.
681
682 Raises:
683 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400684 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400685 """
686 if http is None:
687 http = self.http
Joe Gregorio9086bd32013-06-14 16:32:05 -0400688
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500689 if self.resumable:
690 body = None
691 while body is None:
Joe Gregorio9086bd32013-06-14 16:32:05 -0400692 _, body = self.next_chunk(http=http, num_retries=num_retries)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500693 return body
Joe Gregorio9086bd32013-06-14 16:32:05 -0400694
695 # Non-resumable case.
696
697 if 'content-length' not in self.headers:
698 self.headers['content-length'] = str(self.body_size)
699 # If the request URI is too long then turn it into a POST request.
700 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
701 self.method = 'POST'
702 self.headers['x-http-method-override'] = 'GET'
703 self.headers['content-type'] = 'application/x-www-form-urlencoded'
704 parsed = urlparse.urlparse(self.uri)
705 self.uri = urlparse.urlunparse(
706 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
707 None)
708 )
709 self.body = parsed.query
710 self.headers['content-length'] = str(len(self.body))
711
712 # Handle retries for server-side errors.
713 for retry_num in xrange(num_retries + 1):
714 if retry_num > 0:
715 self._sleep(self._rand() * 2**retry_num)
716 logging.warning('Retry #%d for request: %s %s, following status: %d'
717 % (retry_num, self.method, self.uri, resp.status))
Joe Gregorioba5c7902012-08-03 12:48:16 -0400718
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500719 resp, content = http.request(str(self.uri), method=str(self.method),
720 body=self.body, headers=self.headers)
Joe Gregorio9086bd32013-06-14 16:32:05 -0400721 if resp.status < 500:
722 break
723
724 for callback in self.response_callbacks:
725 callback(resp)
726 if resp.status >= 300:
727 raise HttpError(resp, content, uri=self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400728 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500729
Ali Afshar164f37e2013-01-07 14:05:45 -0800730 @util.positional(2)
731 def add_response_callback(self, cb):
732 """add_response_headers_callback
733
734 Args:
735 cb: Callback to be called on receiving the response headers, of signature:
736
737 def cb(resp):
738 # Where resp is an instance of httplib2.Response
739 """
740 self.response_callbacks.append(cb)
741
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400742 @util.positional(1)
Joe Gregorio9086bd32013-06-14 16:32:05 -0400743 def next_chunk(self, http=None, num_retries=0):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500744 """Execute the next step of a resumable upload.
745
Joe Gregorio66f57522011-11-30 11:00:00 -0500746 Can only be used if the method being executed supports media uploads and
747 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500748
749 Example:
750
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400751 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio66f57522011-11-30 11:00:00 -0500752 chunksize=1000, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400753 request = farm.animals().insert(
754 id='cow',
755 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500756 media_body=media)
757
758 response = None
759 while response is None:
760 status, response = request.next_chunk()
761 if status:
762 print "Upload %d%% complete." % int(status.progress() * 100)
763
764
Joe Gregorio9086bd32013-06-14 16:32:05 -0400765 Args:
766 http: httplib2.Http, an http object to be used in place of the
767 one the HttpRequest request object was constructed with.
768 num_retries: Integer, number of times to retry 500's with randomized
769 exponential backoff. If all retries fail, the raised HttpError
770 represents the last request. If zero (default), we attempt the
771 request only once.
772
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500773 Returns:
774 (status, body): (ResumableMediaStatus, object)
775 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400776
777 Raises:
778 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400779 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500780 """
781 if http is None:
782 http = self.http
783
Joe Gregorio910b9b12012-06-12 09:36:30 -0400784 if self.resumable.size() is None:
785 size = '*'
786 else:
787 size = str(self.resumable.size())
788
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500789 if self.resumable_uri is None:
790 start_headers = copy.copy(self.headers)
791 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400792 if size != '*':
793 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500794 start_headers['content-length'] = str(self.body_size)
795
Joe Gregorio9086bd32013-06-14 16:32:05 -0400796 for retry_num in xrange(num_retries + 1):
797 if retry_num > 0:
798 self._sleep(self._rand() * 2**retry_num)
799 logging.warning(
800 'Retry #%d for resumable URI request: %s %s, following status: %d'
801 % (retry_num, self.method, self.uri, resp.status))
802
803 resp, content = http.request(self.uri, method=self.method,
804 body=self.body,
805 headers=start_headers)
806 if resp.status < 500:
807 break
808
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500809 if resp.status == 200 and 'location' in resp:
810 self.resumable_uri = resp['location']
811 else:
Joe Gregoriobaf04802013-03-01 12:27:06 -0500812 raise ResumableUploadError(resp, content)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400813 elif self._in_error_state:
814 # If we are in an error state then query the server for current state of
815 # the upload by sending an empty PUT and reading the 'range' header in
816 # the response.
817 headers = {
818 'Content-Range': 'bytes */%s' % size,
819 'content-length': '0'
820 }
821 resp, content = http.request(self.resumable_uri, 'PUT',
822 headers=headers)
823 status, body = self._process_response(resp, content)
824 if body:
825 # The upload was complete.
826 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500827
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400828 # The httplib.request method can take streams for the body parameter, but
829 # only in Python 2.6 or later. If a stream is available under those
830 # conditions then use it as the body argument.
831 if self.resumable.has_stream() and sys.version_info[1] >= 6:
832 data = self.resumable.stream()
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400833 if self.resumable.chunksize() == -1:
Joe Gregorio5c120db2012-08-23 09:13:55 -0400834 data.seek(self.resumable_progress)
835 chunk_end = self.resumable.size() - self.resumable_progress - 1
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400836 else:
Joe Gregorio5c120db2012-08-23 09:13:55 -0400837 # Doing chunking with a stream, so wrap a slice of the stream.
838 data = _StreamSlice(data, self.resumable_progress,
839 self.resumable.chunksize())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400840 chunk_end = min(
841 self.resumable_progress + self.resumable.chunksize() - 1,
842 self.resumable.size() - 1)
843 else:
844 data = self.resumable.getbytes(
845 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400846
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400847 # A short read implies that we are at EOF, so finish the upload.
848 if len(data) < self.resumable.chunksize():
849 size = str(self.resumable_progress + len(data))
850
851 chunk_end = self.resumable_progress + len(data) - 1
Joe Gregorio44454e42012-06-15 08:38:53 -0400852
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500853 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400854 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregorio5c120db2012-08-23 09:13:55 -0400855 self.resumable_progress, chunk_end, size),
856 # Must set the content-length header here because httplib can't
857 # calculate the size when working with _StreamSlice.
858 'Content-Length': str(chunk_end - self.resumable_progress + 1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500859 }
Joe Gregorio9086bd32013-06-14 16:32:05 -0400860
861 for retry_num in xrange(num_retries + 1):
862 if retry_num > 0:
863 self._sleep(self._rand() * 2**retry_num)
864 logging.warning(
865 'Retry #%d for media upload: %s %s, following status: %d'
866 % (retry_num, self.method, self.uri, resp.status))
867
868 try:
869 resp, content = http.request(self.resumable_uri, method='PUT',
870 body=data,
871 headers=headers)
872 except:
873 self._in_error_state = True
874 raise
875 if resp.status < 500:
876 break
Joe Gregorio910b9b12012-06-12 09:36:30 -0400877
878 return self._process_response(resp, content)
879
880 def _process_response(self, resp, content):
881 """Process the response from a single chunk upload.
882
883 Args:
884 resp: httplib2.Response, the response object.
885 content: string, the content of the response.
886
887 Returns:
888 (status, body): (ResumableMediaStatus, object)
889 The body will be None until the resumable media is fully uploaded.
890
891 Raises:
892 apiclient.errors.HttpError if the response was not a 2xx or a 308.
893 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500894 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400895 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500896 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500897 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400898 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500899 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500900 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500901 if 'location' in resp:
902 self.resumable_uri = resp['location']
903 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400904 self._in_error_state = True
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400905 raise HttpError(resp, content, uri=self.uri)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500906
Joe Gregorio945be3e2012-01-27 17:01:06 -0500907 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
908 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500909
910 def to_json(self):
911 """Returns a JSON representation of the HttpRequest."""
912 d = copy.copy(self.__dict__)
913 if d['resumable'] is not None:
914 d['resumable'] = self.resumable.to_json()
915 del d['http']
916 del d['postproc']
Joe Gregorio9086bd32013-06-14 16:32:05 -0400917 del d['_sleep']
918 del d['_rand']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400919
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500920 return simplejson.dumps(d)
921
922 @staticmethod
923 def from_json(s, http, postproc):
924 """Returns an HttpRequest populated with info from a JSON object."""
925 d = simplejson.loads(s)
926 if d['resumable'] is not None:
927 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
928 return HttpRequest(
929 http,
930 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500931 uri=d['uri'],
932 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500933 body=d['body'],
934 headers=d['headers'],
935 methodId=d['methodId'],
936 resumable=d['resumable'])
937
Joe Gregorioaf276d22010-12-09 14:26:58 -0500938
Joe Gregorio66f57522011-11-30 11:00:00 -0500939class BatchHttpRequest(object):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400940 """Batches multiple HttpRequest objects into a single HTTP request.
941
942 Example:
943 from apiclient.http import BatchHttpRequest
944
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400945 def list_animals(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400946 \"\"\"Do something with the animals list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400947 if exception is not None:
948 # Do something with the exception.
949 pass
950 else:
951 # Do something with the response.
952 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400953
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400954 def list_farmers(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400955 \"\"\"Do something with the farmers list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400956 if exception is not None:
957 # Do something with the exception.
958 pass
959 else:
960 # Do something with the response.
961 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400962
963 service = build('farm', 'v2')
964
965 batch = BatchHttpRequest()
966
967 batch.add(service.animals().list(), list_animals)
968 batch.add(service.farmers().list(), list_farmers)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400969 batch.execute(http=http)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400970 """
Joe Gregorio66f57522011-11-30 11:00:00 -0500971
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400972 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -0500973 def __init__(self, callback=None, batch_uri=None):
974 """Constructor for a BatchHttpRequest.
975
976 Args:
977 callback: callable, A callback to be called for each response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -0400978 form callback(id, response, exception). The first parameter is the
979 request id, and the second is the deserialized response object. The
980 third is an apiclient.errors.HttpError exception object if an HTTP error
981 occurred while processing the request, or None if no error occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -0500982 batch_uri: string, URI to send batch requests to.
983 """
984 if batch_uri is None:
985 batch_uri = 'https://www.googleapis.com/batch'
986 self._batch_uri = batch_uri
987
988 # Global callback to be called for each individual response in the batch.
989 self._callback = callback
990
Joe Gregorio654f4a22012-02-09 14:15:44 -0500991 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500992 self._requests = {}
993
Joe Gregorio654f4a22012-02-09 14:15:44 -0500994 # A map from id to callback.
995 self._callbacks = {}
996
Joe Gregorio66f57522011-11-30 11:00:00 -0500997 # List of request ids, in the order in which they were added.
998 self._order = []
999
1000 # The last auto generated id.
1001 self._last_auto_id = 0
1002
1003 # Unique ID on which to base the Content-ID headers.
1004 self._base_id = None
1005
Joe Gregorioc752e332012-07-11 14:43:52 -04001006 # A map from request id to (httplib2.Response, content) response pairs
Joe Gregorio654f4a22012-02-09 14:15:44 -05001007 self._responses = {}
1008
1009 # A map of id(Credentials) that have been refreshed.
1010 self._refreshed_credentials = {}
1011
1012 def _refresh_and_apply_credentials(self, request, http):
1013 """Refresh the credentials and apply to the request.
1014
1015 Args:
1016 request: HttpRequest, the request.
1017 http: httplib2.Http, the global http object for the batch.
1018 """
1019 # For the credentials to refresh, but only once per refresh_token
1020 # If there is no http per the request then refresh the http passed in
1021 # via execute()
1022 creds = None
1023 if request.http is not None and hasattr(request.http.request,
1024 'credentials'):
1025 creds = request.http.request.credentials
1026 elif http is not None and hasattr(http.request, 'credentials'):
1027 creds = http.request.credentials
1028 if creds is not None:
1029 if id(creds) not in self._refreshed_credentials:
1030 creds.refresh(http)
1031 self._refreshed_credentials[id(creds)] = 1
1032
1033 # Only apply the credentials if we are using the http object passed in,
1034 # otherwise apply() will get called during _serialize_request().
1035 if request.http is None or not hasattr(request.http.request,
1036 'credentials'):
1037 creds.apply(request.headers)
1038
Joe Gregorio66f57522011-11-30 11:00:00 -05001039 def _id_to_header(self, id_):
1040 """Convert an id to a Content-ID header value.
1041
1042 Args:
1043 id_: string, identifier of individual request.
1044
1045 Returns:
1046 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1047 the value because Content-ID headers are supposed to be universally
1048 unique.
1049 """
1050 if self._base_id is None:
1051 self._base_id = uuid.uuid4()
1052
1053 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
1054
1055 def _header_to_id(self, header):
1056 """Convert a Content-ID header value to an id.
1057
1058 Presumes the Content-ID header conforms to the format that _id_to_header()
1059 returns.
1060
1061 Args:
1062 header: string, Content-ID header value.
1063
1064 Returns:
1065 The extracted id value.
1066
1067 Raises:
1068 BatchError if the header is not in the expected format.
1069 """
1070 if header[0] != '<' or header[-1] != '>':
1071 raise BatchError("Invalid value for Content-ID: %s" % header)
1072 if '+' not in header:
1073 raise BatchError("Invalid value for Content-ID: %s" % header)
1074 base, id_ = header[1:-1].rsplit('+', 1)
1075
1076 return urllib.unquote(id_)
1077
1078 def _serialize_request(self, request):
1079 """Convert an HttpRequest object into a string.
1080
1081 Args:
1082 request: HttpRequest, the request to serialize.
1083
1084 Returns:
1085 The request as a string in application/http format.
1086 """
1087 # Construct status line
1088 parsed = urlparse.urlparse(request.uri)
1089 request_line = urlparse.urlunparse(
1090 (None, None, parsed.path, parsed.params, parsed.query, None)
1091 )
1092 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001093 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -05001094 msg = MIMENonMultipart(major, minor)
1095 headers = request.headers.copy()
1096
Joe Gregorio654f4a22012-02-09 14:15:44 -05001097 if request.http is not None and hasattr(request.http.request,
1098 'credentials'):
1099 request.http.request.credentials.apply(headers)
1100
Joe Gregorio66f57522011-11-30 11:00:00 -05001101 # MIMENonMultipart adds its own Content-Type header.
1102 if 'content-type' in headers:
1103 del headers['content-type']
1104
1105 for key, value in headers.iteritems():
1106 msg[key] = value
1107 msg['Host'] = parsed.netloc
1108 msg.set_unixfrom(None)
1109
1110 if request.body is not None:
1111 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001112 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -05001113
Joe Gregorio654f4a22012-02-09 14:15:44 -05001114 # Serialize the mime message.
1115 fp = StringIO.StringIO()
1116 # maxheaderlen=0 means don't line wrap headers.
1117 g = Generator(fp, maxheaderlen=0)
1118 g.flatten(msg, unixfrom=False)
1119 body = fp.getvalue()
1120
Joe Gregorio66f57522011-11-30 11:00:00 -05001121 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
1122 if request.body is None:
1123 body = body[:-2]
1124
Joe Gregoriodd813822012-01-25 10:32:47 -05001125 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -05001126
1127 def _deserialize_response(self, payload):
1128 """Convert string into httplib2 response and content.
1129
1130 Args:
1131 payload: string, headers and body as a string.
1132
1133 Returns:
Joe Gregorioc752e332012-07-11 14:43:52 -04001134 A pair (resp, content), such as would be returned from httplib2.request.
Joe Gregorio66f57522011-11-30 11:00:00 -05001135 """
1136 # Strip off the status line
1137 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001138 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001139
1140 # Parse the rest of the response
1141 parser = FeedParser()
1142 parser.feed(payload)
1143 msg = parser.close()
1144 msg['status'] = status
1145
1146 # Create httplib2.Response from the parsed headers.
1147 resp = httplib2.Response(msg)
1148 resp.reason = reason
1149 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1150
1151 content = payload.split('\r\n\r\n', 1)[1]
1152
1153 return resp, content
1154
1155 def _new_id(self):
1156 """Create a new id.
1157
1158 Auto incrementing number that avoids conflicts with ids already used.
1159
1160 Returns:
1161 string, a new unique id.
1162 """
1163 self._last_auto_id += 1
1164 while str(self._last_auto_id) in self._requests:
1165 self._last_auto_id += 1
1166 return str(self._last_auto_id)
1167
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001168 @util.positional(2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001169 def add(self, request, callback=None, request_id=None):
1170 """Add a new request.
1171
1172 Every callback added will be paired with a unique id, the request_id. That
1173 unique id will be passed back to the callback when the response comes back
1174 from the server. The default behavior is to have the library generate it's
1175 own unique id. If the caller passes in a request_id then they must ensure
1176 uniqueness for each request_id, and if they are not an exception is
1177 raised. Callers should either supply all request_ids or nevery supply a
1178 request id, to avoid such an error.
1179
1180 Args:
1181 request: HttpRequest, Request to add to the batch.
1182 callback: callable, A callback to be called for this response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -04001183 form callback(id, response, exception). The first parameter is the
1184 request id, and the second is the deserialized response object. The
1185 third is an apiclient.errors.HttpError exception object if an HTTP error
1186 occurred while processing the request, or None if no errors occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -05001187 request_id: string, A unique id for the request. The id will be passed to
1188 the callback with the response.
1189
1190 Returns:
1191 None
1192
1193 Raises:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001194 BatchError if a media request is added to a batch.
Joe Gregorio66f57522011-11-30 11:00:00 -05001195 KeyError is the request_id is not unique.
1196 """
1197 if request_id is None:
1198 request_id = self._new_id()
1199 if request.resumable is not None:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001200 raise BatchError("Media requests cannot be used in a batch request.")
Joe Gregorio66f57522011-11-30 11:00:00 -05001201 if request_id in self._requests:
1202 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001203 self._requests[request_id] = request
1204 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001205 self._order.append(request_id)
1206
Joe Gregorio654f4a22012-02-09 14:15:44 -05001207 def _execute(self, http, order, requests):
1208 """Serialize batch request, send to server, process response.
1209
1210 Args:
1211 http: httplib2.Http, an http object to be used to make the request with.
1212 order: list, list of request ids in the order they were added to the
1213 batch.
1214 request: list, list of request objects to send.
1215
1216 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001217 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio654f4a22012-02-09 14:15:44 -05001218 apiclient.errors.BatchError if the response is the wrong format.
1219 """
1220 message = MIMEMultipart('mixed')
1221 # Message should not write out it's own headers.
1222 setattr(message, '_write_headers', lambda self: None)
1223
1224 # Add all the individual requests.
1225 for request_id in order:
1226 request = requests[request_id]
1227
1228 msg = MIMENonMultipart('application', 'http')
1229 msg['Content-Transfer-Encoding'] = 'binary'
1230 msg['Content-ID'] = self._id_to_header(request_id)
1231
1232 body = self._serialize_request(request)
1233 msg.set_payload(body)
1234 message.attach(msg)
1235
1236 body = message.as_string()
1237
1238 headers = {}
1239 headers['content-type'] = ('multipart/mixed; '
1240 'boundary="%s"') % message.get_boundary()
1241
Joe Gregorio28f34e72013-04-30 16:29:33 -04001242 resp, content = http.request(self._batch_uri, method='POST', body=body,
Joe Gregorio654f4a22012-02-09 14:15:44 -05001243 headers=headers)
1244
1245 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001246 raise HttpError(resp, content, uri=self._batch_uri)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001247
1248 # Now break out the individual responses and store each one.
1249 boundary, _ = content.split(None, 1)
1250
1251 # Prepend with a content-type header so FeedParser can handle it.
1252 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1253 for_parser = header + content
1254
1255 parser = FeedParser()
1256 parser.feed(for_parser)
1257 mime_response = parser.close()
1258
1259 if not mime_response.is_multipart():
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001260 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1261 content=content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001262
1263 for part in mime_response.get_payload():
1264 request_id = self._header_to_id(part['Content-ID'])
Joe Gregorioc752e332012-07-11 14:43:52 -04001265 response, content = self._deserialize_response(part.get_payload())
1266 self._responses[request_id] = (response, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001267
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001268 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -05001269 def execute(self, http=None):
1270 """Execute all the requests as a single batched HTTP request.
1271
1272 Args:
1273 http: httplib2.Http, an http object to be used in place of the one the
Joe Gregorioe2233cd2013-01-24 15:46:23 -05001274 HttpRequest request object was constructed with. If one isn't supplied
Joe Gregorio66f57522011-11-30 11:00:00 -05001275 then use a http object from the requests in this batch.
1276
1277 Returns:
1278 None
1279
1280 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001281 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001282 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001283 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001284
1285 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001286 if http is None:
1287 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001288 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001289 if request is not None:
1290 http = request.http
1291 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001292
Joe Gregorio66f57522011-11-30 11:00:00 -05001293 if http is None:
1294 raise ValueError("Missing a valid http object.")
1295
Joe Gregorio654f4a22012-02-09 14:15:44 -05001296 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001297
Joe Gregorio654f4a22012-02-09 14:15:44 -05001298 # Loop over all the requests and check for 401s. For each 401 request the
1299 # credentials should be refreshed and then sent again in a separate batch.
1300 redo_requests = {}
1301 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001302
Joe Gregorio66f57522011-11-30 11:00:00 -05001303 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001304 resp, content = self._responses[request_id]
1305 if resp['status'] == '401':
Joe Gregorio654f4a22012-02-09 14:15:44 -05001306 redo_order.append(request_id)
1307 request = self._requests[request_id]
1308 self._refresh_and_apply_credentials(request, http)
1309 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001310
Joe Gregorio654f4a22012-02-09 14:15:44 -05001311 if redo_requests:
1312 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001313
Joe Gregorio654f4a22012-02-09 14:15:44 -05001314 # Now process all callbacks that are erroring, and raise an exception for
1315 # ones that return a non-2xx response? Or add extra parameter to callback
1316 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001317
Joe Gregorio654f4a22012-02-09 14:15:44 -05001318 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001319 resp, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001320
Joe Gregorio654f4a22012-02-09 14:15:44 -05001321 request = self._requests[request_id]
1322 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001323
Joe Gregorio654f4a22012-02-09 14:15:44 -05001324 response = None
1325 exception = None
1326 try:
Joe Gregorio3fb93672012-07-25 11:31:11 -04001327 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001328 raise HttpError(resp, content, uri=request.uri)
Joe Gregorioc752e332012-07-11 14:43:52 -04001329 response = request.postproc(resp, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001330 except HttpError, e:
1331 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001332
Joe Gregorio654f4a22012-02-09 14:15:44 -05001333 if callback is not None:
1334 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001335 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001336 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001337
1338
Joe Gregorioaf276d22010-12-09 14:26:58 -05001339class HttpRequestMock(object):
1340 """Mock of HttpRequest.
1341
1342 Do not construct directly, instead use RequestMockBuilder.
1343 """
1344
1345 def __init__(self, resp, content, postproc):
1346 """Constructor for HttpRequestMock
1347
1348 Args:
1349 resp: httplib2.Response, the response to emulate coming from the request
1350 content: string, the response body
1351 postproc: callable, the post processing function usually supplied by
1352 the model class. See model.JsonModel.response() as an example.
1353 """
1354 self.resp = resp
1355 self.content = content
1356 self.postproc = postproc
1357 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001358 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001359 if 'reason' in self.resp:
1360 self.resp.reason = self.resp['reason']
1361
1362 def execute(self, http=None):
1363 """Execute the request.
1364
1365 Same behavior as HttpRequest.execute(), but the response is
1366 mocked and not really from an HTTP request/response.
1367 """
1368 return self.postproc(self.resp, self.content)
1369
1370
1371class RequestMockBuilder(object):
1372 """A simple mock of HttpRequest
1373
1374 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001375 tuples of (httplib2.Response, content, opt_expected_body) that should be
1376 returned when that method is called. None may also be passed in for the
1377 httplib2.Response, in which case a 200 OK response will be generated.
1378 If an opt_expected_body (str or dict) is provided, it will be compared to
1379 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001380
1381 Example:
1382 response = '{"data": {"id": "tag:google.c...'
1383 requestBuilder = RequestMockBuilder(
1384 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001385 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001386 }
1387 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001388 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001389
1390 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001391 200 OK with an empty string as the response content or raise an excpetion
1392 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001393 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001394
1395 For more details see the project wiki.
1396 """
1397
Joe Gregorioa388ce32011-09-09 17:19:13 -04001398 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001399 """Constructor for RequestMockBuilder
1400
1401 The constructed object should be a callable object
1402 that can replace the class HttpResponse.
1403
1404 responses - A dictionary that maps methodIds into tuples
1405 of (httplib2.Response, content). The methodId
1406 comes from the 'rpcName' field in the discovery
1407 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001408 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1409 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001410 """
1411 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001412 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001413
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001414 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001415 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001416 """Implements the callable interface that discovery.build() expects
1417 of requestBuilder, which is to build an object compatible with
1418 HttpRequest.execute(). See that method for the description of the
1419 parameters and the expected response.
1420 """
1421 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001422 response = self.responses[methodId]
1423 resp, content = response[:2]
1424 if len(response) > 2:
1425 # Test the body against the supplied expected_body.
1426 expected_body = response[2]
1427 if bool(expected_body) != bool(body):
1428 # Not expecting a body and provided one
1429 # or expecting a body and not provided one.
1430 raise UnexpectedBodyError(expected_body, body)
1431 if isinstance(expected_body, str):
1432 expected_body = simplejson.loads(expected_body)
1433 body = simplejson.loads(body)
1434 if body != expected_body:
1435 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001436 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001437 elif self.check_unexpected:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001438 raise UnexpectedMethodError(methodId=methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001439 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001440 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001441 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001442
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001443
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001444class HttpMock(object):
1445 """Mock of httplib2.Http"""
1446
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001447 def __init__(self, filename=None, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001448 """
1449 Args:
1450 filename: string, absolute filename to read response from
1451 headers: dict, header to return with response
1452 """
Joe Gregorioec343652011-02-16 16:52:51 -05001453 if headers is None:
1454 headers = {'status': '200 OK'}
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001455 if filename:
1456 f = file(filename, 'r')
1457 self.data = f.read()
1458 f.close()
1459 else:
1460 self.data = None
1461 self.response_headers = headers
1462 self.headers = None
1463 self.uri = None
1464 self.method = None
1465 self.body = None
1466 self.headers = None
1467
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001468
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001469 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001470 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001471 body=None,
1472 headers=None,
1473 redirections=1,
1474 connection_type=None):
Joe Gregorio83f2ee62012-12-06 15:25:54 -05001475 self.uri = uri
1476 self.method = method
1477 self.body = body
1478 self.headers = headers
1479 return httplib2.Response(self.response_headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001480
1481
1482class HttpMockSequence(object):
1483 """Mock of httplib2.Http
1484
1485 Mocks a sequence of calls to request returning different responses for each
1486 call. Create an instance initialized with the desired response headers
1487 and content and then use as if an httplib2.Http instance.
1488
1489 http = HttpMockSequence([
1490 ({'status': '401'}, ''),
1491 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1492 ({'status': '200'}, 'echo_request_headers'),
1493 ])
1494 resp, content = http.request("http://examples.com")
1495
1496 There are special values you can pass in for content to trigger
1497 behavours that are helpful in testing.
1498
1499 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001500 'echo_request_headers_as_json' means return the request headers in
1501 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001502 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001503 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001504 """
1505
1506 def __init__(self, iterable):
1507 """
1508 Args:
1509 iterable: iterable, a sequence of pairs of (headers, body)
1510 """
1511 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001512 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001513
1514 def request(self, uri,
1515 method='GET',
1516 body=None,
1517 headers=None,
1518 redirections=1,
1519 connection_type=None):
1520 resp, content = self._iterable.pop(0)
1521 if content == 'echo_request_headers':
1522 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001523 elif content == 'echo_request_headers_as_json':
1524 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001525 elif content == 'echo_request_body':
Joe Gregorioc80ac9d2012-08-21 14:09:09 -04001526 if hasattr(body, 'read'):
1527 content = body.read()
1528 else:
1529 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001530 elif content == 'echo_request_uri':
1531 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001532 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001533
1534
1535def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001536 """Set the user-agent on every request.
1537
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001538 Args:
1539 http - An instance of httplib2.Http
1540 or something that acts like it.
1541 user_agent: string, the value for the user-agent header.
1542
1543 Returns:
1544 A modified instance of http that was passed in.
1545
1546 Example:
1547
1548 h = httplib2.Http()
1549 h = set_user_agent(h, "my-app-name/6.0")
1550
1551 Most of the time the user-agent will be set doing auth, this is for the rare
1552 cases where you are accessing an unauthenticated endpoint.
1553 """
1554 request_orig = http.request
1555
1556 # The closure that will replace 'httplib2.Http.request'.
1557 def new_request(uri, method='GET', body=None, headers=None,
1558 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1559 connection_type=None):
1560 """Modify the request headers to add the user-agent."""
1561 if headers is None:
1562 headers = {}
1563 if 'user-agent' in headers:
1564 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1565 else:
1566 headers['user-agent'] = user_agent
1567 resp, content = request_orig(uri, method, body, headers,
1568 redirections, connection_type)
1569 return resp, content
1570
1571 http.request = new_request
1572 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001573
1574
1575def tunnel_patch(http):
1576 """Tunnel PATCH requests over POST.
1577 Args:
1578 http - An instance of httplib2.Http
1579 or something that acts like it.
1580
1581 Returns:
1582 A modified instance of http that was passed in.
1583
1584 Example:
1585
1586 h = httplib2.Http()
1587 h = tunnel_patch(h, "my-app-name/6.0")
1588
1589 Useful if you are running on a platform that doesn't support PATCH.
1590 Apply this last if you are using OAuth 1.0, as changing the method
1591 will result in a different signature.
1592 """
1593 request_orig = http.request
1594
1595 # The closure that will replace 'httplib2.Http.request'.
1596 def new_request(uri, method='GET', body=None, headers=None,
1597 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1598 connection_type=None):
1599 """Modify the request headers to add the user-agent."""
1600 if headers is None:
1601 headers = {}
1602 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001603 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001604 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001605 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001606 headers['x-http-method-override'] = "PATCH"
1607 method = 'POST'
1608 resp, content = request_orig(uri, method, body, headers,
1609 redirections, connection_type)
1610 return resp, content
1611
1612 http.request = new_request
1613 return http