blob: 4331cc267a2057bd23e4288260e82ffe1958747d [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
32import urllib
33import urlparse
34import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050035
Joe Gregorio654f4a22012-02-09 14:15:44 -050036from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050037from email.mime.multipart import MIMEMultipart
38from email.mime.nonmultipart import MIMENonMultipart
39from email.parser import FeedParser
40from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050041from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050042from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040043from errors import UnexpectedBodyError
44from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050045from model import JsonModel
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040046from oauth2client import util
Joe Gregorio549230c2012-01-11 10:38:05 -050047from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040048
49
Joe Gregorio910b9b12012-06-12 09:36:30 -040050DEFAULT_CHUNK_SIZE = 512*1024
51
Joe Gregorioba5c7902012-08-03 12:48:16 -040052MAX_URI_LENGTH = 4000
53
Joe Gregorio910b9b12012-06-12 09:36:30 -040054
Joe Gregoriod0bd3882011-11-22 09:49:47 -050055class MediaUploadProgress(object):
56 """Status of a resumable upload."""
57
58 def __init__(self, resumable_progress, total_size):
59 """Constructor.
60
61 Args:
62 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040063 total_size: int, total bytes in complete upload, or None if the total
64 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050065 """
66 self.resumable_progress = resumable_progress
67 self.total_size = total_size
68
69 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040070 """Percent of upload completed, as a float.
71
72 Returns:
73 the percentage complete as a float, returning 0.0 if the total size of
74 the upload is unknown.
75 """
76 if self.total_size is not None:
77 return float(self.resumable_progress) / float(self.total_size)
78 else:
79 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050080
81
Joe Gregorio708388c2012-06-15 13:43:04 -040082class MediaDownloadProgress(object):
83 """Status of a resumable download."""
84
85 def __init__(self, resumable_progress, total_size):
86 """Constructor.
87
88 Args:
89 resumable_progress: int, bytes received so far.
90 total_size: int, total bytes in complete download.
91 """
92 self.resumable_progress = resumable_progress
93 self.total_size = total_size
94
95 def progress(self):
96 """Percent of download completed, as a float.
97
98 Returns:
99 the percentage complete as a float, returning 0.0 if the total size of
100 the download is unknown.
101 """
102 if self.total_size is not None:
103 return float(self.resumable_progress) / float(self.total_size)
104 else:
105 return 0.0
106
107
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500108class MediaUpload(object):
109 """Describes a media object to upload.
110
111 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400112
113 Note that subclasses of MediaUpload may allow you to control the chunksize
114 when upload a media object. It is important to keep the size of the chunk as
115 large as possible to keep the upload efficient. Other factors may influence
116 the size of the chunk you use, particularly if you are working in an
117 environment where individual HTTP requests may have a hardcoded time limit,
118 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500119 """
120
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500121 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400122 """Chunk size for resumable uploads.
123
124 Returns:
125 Chunk size in bytes.
126 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500127 raise NotImplementedError()
128
129 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400130 """Mime type of the body.
131
132 Returns:
133 Mime type.
134 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500135 return 'application/octet-stream'
136
Joe Gregorio910b9b12012-06-12 09:36:30 -0400137 def size(self):
138 """Size of upload.
139
140 Returns:
141 Size of the body, or None of the size is unknown.
142 """
143 return None
144
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500145 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400146 """Whether this upload is resumable.
147
148 Returns:
149 True if resumable upload or False.
150 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500151 return False
152
Joe Gregorio910b9b12012-06-12 09:36:30 -0400153 def getbytes(self, begin, end):
154 """Get bytes from the media.
155
156 Args:
157 begin: int, offset from beginning of file.
158 length: int, number of bytes to read, starting at begin.
159
160 Returns:
161 A string of bytes read. May be shorter than length if EOF was reached
162 first.
163 """
164 raise NotImplementedError()
165
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400166 @util.positional(1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500167 def _to_json(self, strip=None):
168 """Utility function for creating a JSON representation of a MediaUpload.
169
170 Args:
171 strip: array, An array of names of members to not include in the JSON.
172
173 Returns:
174 string, a JSON representation of this instance, suitable to pass to
175 from_json().
176 """
177 t = type(self)
178 d = copy.copy(self.__dict__)
179 if strip is not None:
180 for member in strip:
181 del d[member]
182 d['_class'] = t.__name__
183 d['_module'] = t.__module__
184 return simplejson.dumps(d)
185
186 def to_json(self):
187 """Create a JSON representation of an instance of MediaUpload.
188
189 Returns:
190 string, a JSON representation of this instance, suitable to pass to
191 from_json().
192 """
193 return self._to_json()
194
195 @classmethod
196 def new_from_json(cls, s):
197 """Utility class method to instantiate a MediaUpload subclass from a JSON
198 representation produced by to_json().
199
200 Args:
201 s: string, JSON from to_json().
202
203 Returns:
204 An instance of the subclass of MediaUpload that was serialized with
205 to_json().
206 """
207 data = simplejson.loads(s)
208 # Find and call the right classmethod from_json() to restore the object.
209 module = data['_module']
210 m = __import__(module, fromlist=module.split('.')[:-1])
211 kls = getattr(m, data['_class'])
212 from_json = getattr(kls, 'from_json')
213 return from_json(s)
214
Joe Gregorio66f57522011-11-30 11:00:00 -0500215
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500216class MediaFileUpload(MediaUpload):
217 """A MediaUpload for a file.
218
219 Construct a MediaFileUpload and pass as the media_body parameter of the
220 method. For example, if we had a service that allowed uploading images:
221
222
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400223 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400224 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400225 farm.animals()..insert(
226 id='cow',
227 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500228 media_body=media).execute()
229 """
230
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400231 @util.positional(2)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400232 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500233 """Constructor.
234
235 Args:
236 filename: string, Name of the file.
237 mimetype: string, Mime-type of the file. If None then a mime-type will be
238 guessed from the file extension.
239 chunksize: int, File will be uploaded in chunks of this many bytes. Only
240 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500241 resumable: bool, True if this is a resumable upload. False means upload
242 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500243 """
244 self._filename = filename
245 self._size = os.path.getsize(filename)
246 self._fd = None
247 if mimetype is None:
248 (mimetype, encoding) = mimetypes.guess_type(filename)
249 self._mimetype = mimetype
250 self._chunksize = chunksize
251 self._resumable = resumable
252
Joe Gregorio910b9b12012-06-12 09:36:30 -0400253 def chunksize(self):
254 """Chunk size for resumable uploads.
255
256 Returns:
257 Chunk size in bytes.
258 """
259 return self._chunksize
260
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500261 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400262 """Mime type of the body.
263
264 Returns:
265 Mime type.
266 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500267 return self._mimetype
268
269 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400270 """Size of upload.
271
272 Returns:
273 Size of the body, or None of the size is unknown.
274 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500275 return self._size
276
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500277 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400278 """Whether this upload is resumable.
279
280 Returns:
281 True if resumable upload or False.
282 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500283 return self._resumable
284
285 def getbytes(self, begin, length):
286 """Get bytes from the media.
287
288 Args:
289 begin: int, offset from beginning of file.
290 length: int, number of bytes to read, starting at begin.
291
292 Returns:
293 A string of bytes read. May be shorted than length if EOF was reached
294 first.
295 """
296 if self._fd is None:
297 self._fd = open(self._filename, 'rb')
298 self._fd.seek(begin)
299 return self._fd.read(length)
300
301 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400302 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500303
304 Returns:
305 string, a JSON representation of this instance, suitable to pass to
306 from_json().
307 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400308 return self._to_json(strip=['_fd'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500309
310 @staticmethod
311 def from_json(s):
312 d = simplejson.loads(s)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400313 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
314 chunksize=d['_chunksize'], resumable=d['_resumable'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500315
316
Joe Gregorio910b9b12012-06-12 09:36:30 -0400317class MediaIoBaseUpload(MediaUpload):
318 """A MediaUpload for a io.Base objects.
319
320 Note that the Python file object is compatible with io.Base and can be used
321 with this class also.
322
Joe Gregorio910b9b12012-06-12 09:36:30 -0400323 fh = io.BytesIO('...Some data to upload...')
324 media = MediaIoBaseUpload(fh, mimetype='image/png',
325 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400326 farm.animals().insert(
327 id='cow',
328 name='cow.png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400329 media_body=media).execute()
330 """
331
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400332 @util.positional(3)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400333 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400334 resumable=False):
335 """Constructor.
336
337 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400338 fd: io.Base or file object, The source of the bytes to upload. MUST be
Joe Gregorio44454e42012-06-15 08:38:53 -0400339 opened in blocking mode, do not use streams opened in non-blocking mode.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400340 mimetype: string, Mime-type of the file. If None then a mime-type will be
341 guessed from the file extension.
342 chunksize: int, File will be uploaded in chunks of this many bytes. Only
343 used if resumable=True.
344 resumable: bool, True if this is a resumable upload. False means upload
345 in a single request.
346 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400347 self._fd = fd
Joe Gregorio910b9b12012-06-12 09:36:30 -0400348 self._mimetype = mimetype
349 self._chunksize = chunksize
350 self._resumable = resumable
351 self._size = None
352 try:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400353 if hasattr(self._fd, 'fileno'):
354 fileno = self._fd.fileno()
Joe Gregorio44454e42012-06-15 08:38:53 -0400355
356 # Pipes and such show up as 0 length files.
357 size = os.fstat(fileno).st_size
358 if size:
359 self._size = os.fstat(fileno).st_size
Joe Gregorio910b9b12012-06-12 09:36:30 -0400360 except IOError:
361 pass
362
363 def chunksize(self):
364 """Chunk size for resumable uploads.
365
366 Returns:
367 Chunk size in bytes.
368 """
369 return self._chunksize
370
371 def mimetype(self):
372 """Mime type of the body.
373
374 Returns:
375 Mime type.
376 """
377 return self._mimetype
378
379 def size(self):
380 """Size of upload.
381
382 Returns:
383 Size of the body, or None of the size is unknown.
384 """
385 return self._size
386
387 def resumable(self):
388 """Whether this upload is resumable.
389
390 Returns:
391 True if resumable upload or False.
392 """
393 return self._resumable
394
395 def getbytes(self, begin, length):
396 """Get bytes from the media.
397
398 Args:
399 begin: int, offset from beginning of file.
400 length: int, number of bytes to read, starting at begin.
401
402 Returns:
403 A string of bytes read. May be shorted than length if EOF was reached
404 first.
405 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400406 self._fd.seek(begin)
407 return self._fd.read(length)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400408
409 def to_json(self):
410 """This upload type is not serializable."""
411 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
412
413
Ali Afshar6f11ea12012-02-07 10:32:14 -0500414class MediaInMemoryUpload(MediaUpload):
415 """MediaUpload for a chunk of bytes.
416
417 Construct a MediaFileUpload and pass as the media_body parameter of the
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400418 method.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500419 """
420
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400421 @util.positional(2)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500422 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400423 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500424 """Create a new MediaBytesUpload.
425
426 Args:
427 body: string, Bytes of body content.
428 mimetype: string, Mime-type of the file or default of
429 'application/octet-stream'.
430 chunksize: int, File will be uploaded in chunks of this many bytes. Only
431 used if resumable=True.
432 resumable: bool, True if this is a resumable upload. False means upload
433 in a single request.
434 """
435 self._body = body
436 self._mimetype = mimetype
437 self._resumable = resumable
438 self._chunksize = chunksize
439
440 def chunksize(self):
441 """Chunk size for resumable uploads.
442
443 Returns:
444 Chunk size in bytes.
445 """
446 return self._chunksize
447
448 def mimetype(self):
449 """Mime type of the body.
450
451 Returns:
452 Mime type.
453 """
454 return self._mimetype
455
456 def size(self):
457 """Size of upload.
458
459 Returns:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400460 Size of the body, or None of the size is unknown.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500461 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400462 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500463
464 def resumable(self):
465 """Whether this upload is resumable.
466
467 Returns:
468 True if resumable upload or False.
469 """
470 return self._resumable
471
472 def getbytes(self, begin, length):
473 """Get bytes from the media.
474
475 Args:
476 begin: int, offset from beginning of file.
477 length: int, number of bytes to read, starting at begin.
478
479 Returns:
480 A string of bytes read. May be shorter than length if EOF was reached
481 first.
482 """
483 return self._body[begin:begin + length]
484
485 def to_json(self):
486 """Create a JSON representation of a MediaInMemoryUpload.
487
488 Returns:
489 string, a JSON representation of this instance, suitable to pass to
490 from_json().
491 """
492 t = type(self)
493 d = copy.copy(self.__dict__)
494 del d['_body']
495 d['_class'] = t.__name__
496 d['_module'] = t.__module__
497 d['_b64body'] = base64.b64encode(self._body)
498 return simplejson.dumps(d)
499
500 @staticmethod
501 def from_json(s):
502 d = simplejson.loads(s)
503 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400504 mimetype=d['_mimetype'],
505 chunksize=d['_chunksize'],
506 resumable=d['_resumable'])
Ali Afshar6f11ea12012-02-07 10:32:14 -0500507
508
Joe Gregorio708388c2012-06-15 13:43:04 -0400509class MediaIoBaseDownload(object):
510 """"Download media resources.
511
512 Note that the Python file object is compatible with io.Base and can be used
513 with this class also.
514
515
516 Example:
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400517 request = farms.animals().get_media(id='cow')
518 fh = io.FileIO('cow.png', mode='wb')
Joe Gregorio708388c2012-06-15 13:43:04 -0400519 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
520
521 done = False
522 while done is False:
523 status, done = downloader.next_chunk()
524 if status:
525 print "Download %d%%." % int(status.progress() * 100)
526 print "Download Complete!"
527 """
528
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400529 @util.positional(3)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400530 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
Joe Gregorio708388c2012-06-15 13:43:04 -0400531 """Constructor.
532
533 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400534 fd: io.Base or file object, The stream in which to write the downloaded
Joe Gregorio708388c2012-06-15 13:43:04 -0400535 bytes.
536 request: apiclient.http.HttpRequest, the media request to perform in
537 chunks.
538 chunksize: int, File will be downloaded in chunks of this many bytes.
539 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400540 self._fd = fd
541 self._request = request
542 self._uri = request.uri
543 self._chunksize = chunksize
544 self._progress = 0
545 self._total_size = None
546 self._done = False
Joe Gregorio708388c2012-06-15 13:43:04 -0400547
548 def next_chunk(self):
549 """Get the next chunk of the download.
550
551 Returns:
552 (status, done): (MediaDownloadStatus, boolean)
553 The value of 'done' will be True when the media has been fully
554 downloaded.
555
556 Raises:
557 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400558 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio708388c2012-06-15 13:43:04 -0400559 """
560 headers = {
561 'range': 'bytes=%d-%d' % (
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400562 self._progress, self._progress + self._chunksize)
Joe Gregorio708388c2012-06-15 13:43:04 -0400563 }
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400564 http = self._request.http
Joe Gregorio708388c2012-06-15 13:43:04 -0400565 http.follow_redirects = False
566
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400567 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400568 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400569 self._uri = resp['location']
570 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400571 if resp.status in [200, 206]:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400572 self._progress += len(content)
573 self._fd.write(content)
Joe Gregorio708388c2012-06-15 13:43:04 -0400574
575 if 'content-range' in resp:
576 content_range = resp['content-range']
577 length = content_range.rsplit('/', 1)[1]
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400578 self._total_size = int(length)
Joe Gregorio708388c2012-06-15 13:43:04 -0400579
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400580 if self._progress == self._total_size:
581 self._done = True
582 return MediaDownloadProgress(self._progress, self._total_size), self._done
Joe Gregorio708388c2012-06-15 13:43:04 -0400583 else:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400584 raise HttpError(resp, content, uri=self._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400585
586
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400587class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500588 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400589
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400590 @util.positional(4)
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500591 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500592 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500593 body=None,
594 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500595 methodId=None,
596 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500597 """Constructor for an HttpRequest.
598
Joe Gregorioaf276d22010-12-09 14:26:58 -0500599 Args:
600 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500601 postproc: callable, called on the HTTP response and content to transform
602 it into a data object before returning, or raising an exception
603 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500604 uri: string, the absolute URI to send the request to
605 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500606 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500607 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500608 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500609 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500610 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400611 self.uri = uri
612 self.method = method
613 self.body = body
614 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500615 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400616 self.http = http
617 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500618 self.resumable = resumable
Joe Gregorio910b9b12012-06-12 09:36:30 -0400619 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500620
Joe Gregorio66f57522011-11-30 11:00:00 -0500621 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500622 major, minor, params = mimeparse.parse_mime_type(
623 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500624
Joe Gregorio945be3e2012-01-27 17:01:06 -0500625 # The size of the non-media part of the request.
626 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500627
628 # The resumable URI to send chunks to.
629 self.resumable_uri = None
630
631 # The bytes that have been uploaded.
632 self.resumable_progress = 0
633
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400634 @util.positional(1)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400635 def execute(self, http=None):
636 """Execute the request.
637
Joe Gregorioaf276d22010-12-09 14:26:58 -0500638 Args:
639 http: httplib2.Http, an http object to be used in place of the
640 one the HttpRequest request object was constructed with.
641
642 Returns:
643 A deserialized object model of the response body as determined
644 by the postproc.
645
646 Raises:
647 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400648 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400649 """
650 if http is None:
651 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500652 if self.resumable:
653 body = None
654 while body is None:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400655 _, body = self.next_chunk(http=http)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500656 return body
657 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500658 if 'content-length' not in self.headers:
659 self.headers['content-length'] = str(self.body_size)
Joe Gregorioba5c7902012-08-03 12:48:16 -0400660 # If the request URI is too long then turn it into a POST request.
661 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
662 self.method = 'POST'
663 self.headers['x-http-method-override'] = 'GET'
664 self.headers['content-type'] = 'application/x-www-form-urlencoded'
665 parsed = urlparse.urlparse(self.uri)
666 self.uri = urlparse.urlunparse(
667 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
668 None)
669 )
670 self.body = parsed.query
671 self.headers['content-length'] = str(len(self.body))
672
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400673 resp, content = http.request(self.uri, method=self.method,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500674 body=self.body,
675 headers=self.headers)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500676 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400677 raise HttpError(resp, content, uri=self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400678 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500679
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400680 @util.positional(1)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500681 def next_chunk(self, http=None):
682 """Execute the next step of a resumable upload.
683
Joe Gregorio66f57522011-11-30 11:00:00 -0500684 Can only be used if the method being executed supports media uploads and
685 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500686
687 Example:
688
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400689 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio66f57522011-11-30 11:00:00 -0500690 chunksize=1000, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400691 request = farm.animals().insert(
692 id='cow',
693 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500694 media_body=media)
695
696 response = None
697 while response is None:
698 status, response = request.next_chunk()
699 if status:
700 print "Upload %d%% complete." % int(status.progress() * 100)
701
702
703 Returns:
704 (status, body): (ResumableMediaStatus, object)
705 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400706
707 Raises:
708 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400709 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500710 """
711 if http is None:
712 http = self.http
713
Joe Gregorio910b9b12012-06-12 09:36:30 -0400714 if self.resumable.size() is None:
715 size = '*'
716 else:
717 size = str(self.resumable.size())
718
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500719 if self.resumable_uri is None:
720 start_headers = copy.copy(self.headers)
721 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400722 if size != '*':
723 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500724 start_headers['content-length'] = str(self.body_size)
725
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500726 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500727 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500728 headers=start_headers)
729 if resp.status == 200 and 'location' in resp:
730 self.resumable_uri = resp['location']
731 else:
732 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregorio910b9b12012-06-12 09:36:30 -0400733 elif self._in_error_state:
734 # If we are in an error state then query the server for current state of
735 # the upload by sending an empty PUT and reading the 'range' header in
736 # the response.
737 headers = {
738 'Content-Range': 'bytes */%s' % size,
739 'content-length': '0'
740 }
741 resp, content = http.request(self.resumable_uri, 'PUT',
742 headers=headers)
743 status, body = self._process_response(resp, content)
744 if body:
745 # The upload was complete.
746 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500747
Joe Gregorio910b9b12012-06-12 09:36:30 -0400748 data = self.resumable.getbytes(
749 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400750
751 # A short read implies that we are at EOF, so finish the upload.
752 if len(data) < self.resumable.chunksize():
753 size = str(self.resumable_progress + len(data))
754
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500755 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400756 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500757 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400758 size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500759 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400760 try:
761 resp, content = http.request(self.resumable_uri, 'PUT',
762 body=data,
763 headers=headers)
764 except:
765 self._in_error_state = True
766 raise
767
768 return self._process_response(resp, content)
769
770 def _process_response(self, resp, content):
771 """Process the response from a single chunk upload.
772
773 Args:
774 resp: httplib2.Response, the response object.
775 content: string, the content of the response.
776
777 Returns:
778 (status, body): (ResumableMediaStatus, object)
779 The body will be None until the resumable media is fully uploaded.
780
781 Raises:
782 apiclient.errors.HttpError if the response was not a 2xx or a 308.
783 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500784 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400785 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500786 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500787 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400788 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500789 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500790 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500791 if 'location' in resp:
792 self.resumable_uri = resp['location']
793 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400794 self._in_error_state = True
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400795 raise HttpError(resp, content, uri=self.uri)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500796
Joe Gregorio945be3e2012-01-27 17:01:06 -0500797 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
798 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500799
800 def to_json(self):
801 """Returns a JSON representation of the HttpRequest."""
802 d = copy.copy(self.__dict__)
803 if d['resumable'] is not None:
804 d['resumable'] = self.resumable.to_json()
805 del d['http']
806 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400807
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500808 return simplejson.dumps(d)
809
810 @staticmethod
811 def from_json(s, http, postproc):
812 """Returns an HttpRequest populated with info from a JSON object."""
813 d = simplejson.loads(s)
814 if d['resumable'] is not None:
815 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
816 return HttpRequest(
817 http,
818 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500819 uri=d['uri'],
820 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500821 body=d['body'],
822 headers=d['headers'],
823 methodId=d['methodId'],
824 resumable=d['resumable'])
825
Joe Gregorioaf276d22010-12-09 14:26:58 -0500826
Joe Gregorio66f57522011-11-30 11:00:00 -0500827class BatchHttpRequest(object):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400828 """Batches multiple HttpRequest objects into a single HTTP request.
829
830 Example:
831 from apiclient.http import BatchHttpRequest
832
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400833 def list_animals(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400834 \"\"\"Do something with the animals list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400835 if exception is not None:
836 # Do something with the exception.
837 pass
838 else:
839 # Do something with the response.
840 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400841
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400842 def list_farmers(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400843 \"\"\"Do something with the farmers list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400844 if exception is not None:
845 # Do something with the exception.
846 pass
847 else:
848 # Do something with the response.
849 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400850
851 service = build('farm', 'v2')
852
853 batch = BatchHttpRequest()
854
855 batch.add(service.animals().list(), list_animals)
856 batch.add(service.farmers().list(), list_farmers)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400857 batch.execute(http=http)
Joe Gregorioebd0b842012-06-15 14:14:17 -0400858 """
Joe Gregorio66f57522011-11-30 11:00:00 -0500859
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400860 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -0500861 def __init__(self, callback=None, batch_uri=None):
862 """Constructor for a BatchHttpRequest.
863
864 Args:
865 callback: callable, A callback to be called for each response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -0400866 form callback(id, response, exception). The first parameter is the
867 request id, and the second is the deserialized response object. The
868 third is an apiclient.errors.HttpError exception object if an HTTP error
869 occurred while processing the request, or None if no error occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -0500870 batch_uri: string, URI to send batch requests to.
871 """
872 if batch_uri is None:
873 batch_uri = 'https://www.googleapis.com/batch'
874 self._batch_uri = batch_uri
875
876 # Global callback to be called for each individual response in the batch.
877 self._callback = callback
878
Joe Gregorio654f4a22012-02-09 14:15:44 -0500879 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500880 self._requests = {}
881
Joe Gregorio654f4a22012-02-09 14:15:44 -0500882 # A map from id to callback.
883 self._callbacks = {}
884
Joe Gregorio66f57522011-11-30 11:00:00 -0500885 # List of request ids, in the order in which they were added.
886 self._order = []
887
888 # The last auto generated id.
889 self._last_auto_id = 0
890
891 # Unique ID on which to base the Content-ID headers.
892 self._base_id = None
893
Joe Gregorioc752e332012-07-11 14:43:52 -0400894 # A map from request id to (httplib2.Response, content) response pairs
Joe Gregorio654f4a22012-02-09 14:15:44 -0500895 self._responses = {}
896
897 # A map of id(Credentials) that have been refreshed.
898 self._refreshed_credentials = {}
899
900 def _refresh_and_apply_credentials(self, request, http):
901 """Refresh the credentials and apply to the request.
902
903 Args:
904 request: HttpRequest, the request.
905 http: httplib2.Http, the global http object for the batch.
906 """
907 # For the credentials to refresh, but only once per refresh_token
908 # If there is no http per the request then refresh the http passed in
909 # via execute()
910 creds = None
911 if request.http is not None and hasattr(request.http.request,
912 'credentials'):
913 creds = request.http.request.credentials
914 elif http is not None and hasattr(http.request, 'credentials'):
915 creds = http.request.credentials
916 if creds is not None:
917 if id(creds) not in self._refreshed_credentials:
918 creds.refresh(http)
919 self._refreshed_credentials[id(creds)] = 1
920
921 # Only apply the credentials if we are using the http object passed in,
922 # otherwise apply() will get called during _serialize_request().
923 if request.http is None or not hasattr(request.http.request,
924 'credentials'):
925 creds.apply(request.headers)
926
Joe Gregorio66f57522011-11-30 11:00:00 -0500927 def _id_to_header(self, id_):
928 """Convert an id to a Content-ID header value.
929
930 Args:
931 id_: string, identifier of individual request.
932
933 Returns:
934 A Content-ID header with the id_ encoded into it. A UUID is prepended to
935 the value because Content-ID headers are supposed to be universally
936 unique.
937 """
938 if self._base_id is None:
939 self._base_id = uuid.uuid4()
940
941 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
942
943 def _header_to_id(self, header):
944 """Convert a Content-ID header value to an id.
945
946 Presumes the Content-ID header conforms to the format that _id_to_header()
947 returns.
948
949 Args:
950 header: string, Content-ID header value.
951
952 Returns:
953 The extracted id value.
954
955 Raises:
956 BatchError if the header is not in the expected format.
957 """
958 if header[0] != '<' or header[-1] != '>':
959 raise BatchError("Invalid value for Content-ID: %s" % header)
960 if '+' not in header:
961 raise BatchError("Invalid value for Content-ID: %s" % header)
962 base, id_ = header[1:-1].rsplit('+', 1)
963
964 return urllib.unquote(id_)
965
966 def _serialize_request(self, request):
967 """Convert an HttpRequest object into a string.
968
969 Args:
970 request: HttpRequest, the request to serialize.
971
972 Returns:
973 The request as a string in application/http format.
974 """
975 # Construct status line
976 parsed = urlparse.urlparse(request.uri)
977 request_line = urlparse.urlunparse(
978 (None, None, parsed.path, parsed.params, parsed.query, None)
979 )
980 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500981 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500982 msg = MIMENonMultipart(major, minor)
983 headers = request.headers.copy()
984
Joe Gregorio654f4a22012-02-09 14:15:44 -0500985 if request.http is not None and hasattr(request.http.request,
986 'credentials'):
987 request.http.request.credentials.apply(headers)
988
Joe Gregorio66f57522011-11-30 11:00:00 -0500989 # MIMENonMultipart adds its own Content-Type header.
990 if 'content-type' in headers:
991 del headers['content-type']
992
993 for key, value in headers.iteritems():
994 msg[key] = value
995 msg['Host'] = parsed.netloc
996 msg.set_unixfrom(None)
997
998 if request.body is not None:
999 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001000 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -05001001
Joe Gregorio654f4a22012-02-09 14:15:44 -05001002 # Serialize the mime message.
1003 fp = StringIO.StringIO()
1004 # maxheaderlen=0 means don't line wrap headers.
1005 g = Generator(fp, maxheaderlen=0)
1006 g.flatten(msg, unixfrom=False)
1007 body = fp.getvalue()
1008
Joe Gregorio66f57522011-11-30 11:00:00 -05001009 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
1010 if request.body is None:
1011 body = body[:-2]
1012
Joe Gregoriodd813822012-01-25 10:32:47 -05001013 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -05001014
1015 def _deserialize_response(self, payload):
1016 """Convert string into httplib2 response and content.
1017
1018 Args:
1019 payload: string, headers and body as a string.
1020
1021 Returns:
Joe Gregorioc752e332012-07-11 14:43:52 -04001022 A pair (resp, content), such as would be returned from httplib2.request.
Joe Gregorio66f57522011-11-30 11:00:00 -05001023 """
1024 # Strip off the status line
1025 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001026 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001027
1028 # Parse the rest of the response
1029 parser = FeedParser()
1030 parser.feed(payload)
1031 msg = parser.close()
1032 msg['status'] = status
1033
1034 # Create httplib2.Response from the parsed headers.
1035 resp = httplib2.Response(msg)
1036 resp.reason = reason
1037 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1038
1039 content = payload.split('\r\n\r\n', 1)[1]
1040
1041 return resp, content
1042
1043 def _new_id(self):
1044 """Create a new id.
1045
1046 Auto incrementing number that avoids conflicts with ids already used.
1047
1048 Returns:
1049 string, a new unique id.
1050 """
1051 self._last_auto_id += 1
1052 while str(self._last_auto_id) in self._requests:
1053 self._last_auto_id += 1
1054 return str(self._last_auto_id)
1055
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001056 @util.positional(2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001057 def add(self, request, callback=None, request_id=None):
1058 """Add a new request.
1059
1060 Every callback added will be paired with a unique id, the request_id. That
1061 unique id will be passed back to the callback when the response comes back
1062 from the server. The default behavior is to have the library generate it's
1063 own unique id. If the caller passes in a request_id then they must ensure
1064 uniqueness for each request_id, and if they are not an exception is
1065 raised. Callers should either supply all request_ids or nevery supply a
1066 request id, to avoid such an error.
1067
1068 Args:
1069 request: HttpRequest, Request to add to the batch.
1070 callback: callable, A callback to be called for this response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -04001071 form callback(id, response, exception). The first parameter is the
1072 request id, and the second is the deserialized response object. The
1073 third is an apiclient.errors.HttpError exception object if an HTTP error
1074 occurred while processing the request, or None if no errors occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -05001075 request_id: string, A unique id for the request. The id will be passed to
1076 the callback with the response.
1077
1078 Returns:
1079 None
1080
1081 Raises:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001082 BatchError if a media request is added to a batch.
Joe Gregorio66f57522011-11-30 11:00:00 -05001083 KeyError is the request_id is not unique.
1084 """
1085 if request_id is None:
1086 request_id = self._new_id()
1087 if request.resumable is not None:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001088 raise BatchError("Media requests cannot be used in a batch request.")
Joe Gregorio66f57522011-11-30 11:00:00 -05001089 if request_id in self._requests:
1090 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001091 self._requests[request_id] = request
1092 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001093 self._order.append(request_id)
1094
Joe Gregorio654f4a22012-02-09 14:15:44 -05001095 def _execute(self, http, order, requests):
1096 """Serialize batch request, send to server, process response.
1097
1098 Args:
1099 http: httplib2.Http, an http object to be used to make the request with.
1100 order: list, list of request ids in the order they were added to the
1101 batch.
1102 request: list, list of request objects to send.
1103
1104 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001105 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio654f4a22012-02-09 14:15:44 -05001106 apiclient.errors.BatchError if the response is the wrong format.
1107 """
1108 message = MIMEMultipart('mixed')
1109 # Message should not write out it's own headers.
1110 setattr(message, '_write_headers', lambda self: None)
1111
1112 # Add all the individual requests.
1113 for request_id in order:
1114 request = requests[request_id]
1115
1116 msg = MIMENonMultipart('application', 'http')
1117 msg['Content-Transfer-Encoding'] = 'binary'
1118 msg['Content-ID'] = self._id_to_header(request_id)
1119
1120 body = self._serialize_request(request)
1121 msg.set_payload(body)
1122 message.attach(msg)
1123
1124 body = message.as_string()
1125
1126 headers = {}
1127 headers['content-type'] = ('multipart/mixed; '
1128 'boundary="%s"') % message.get_boundary()
1129
1130 resp, content = http.request(self._batch_uri, 'POST', body=body,
1131 headers=headers)
1132
1133 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001134 raise HttpError(resp, content, uri=self._batch_uri)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001135
1136 # Now break out the individual responses and store each one.
1137 boundary, _ = content.split(None, 1)
1138
1139 # Prepend with a content-type header so FeedParser can handle it.
1140 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1141 for_parser = header + content
1142
1143 parser = FeedParser()
1144 parser.feed(for_parser)
1145 mime_response = parser.close()
1146
1147 if not mime_response.is_multipart():
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001148 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1149 content=content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001150
1151 for part in mime_response.get_payload():
1152 request_id = self._header_to_id(part['Content-ID'])
Joe Gregorioc752e332012-07-11 14:43:52 -04001153 response, content = self._deserialize_response(part.get_payload())
1154 self._responses[request_id] = (response, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001155
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001156 @util.positional(1)
Joe Gregorio66f57522011-11-30 11:00:00 -05001157 def execute(self, http=None):
1158 """Execute all the requests as a single batched HTTP request.
1159
1160 Args:
1161 http: httplib2.Http, an http object to be used in place of the one the
1162 HttpRequest request object was constructed with. If one isn't supplied
1163 then use a http object from the requests in this batch.
1164
1165 Returns:
1166 None
1167
1168 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001169 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001170 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001171 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001172
1173 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001174 if http is None:
1175 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001176 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001177 if request is not None:
1178 http = request.http
1179 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001180
Joe Gregorio66f57522011-11-30 11:00:00 -05001181 if http is None:
1182 raise ValueError("Missing a valid http object.")
1183
Joe Gregorio654f4a22012-02-09 14:15:44 -05001184 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001185
Joe Gregorio654f4a22012-02-09 14:15:44 -05001186 # Loop over all the requests and check for 401s. For each 401 request the
1187 # credentials should be refreshed and then sent again in a separate batch.
1188 redo_requests = {}
1189 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001190
Joe Gregorio66f57522011-11-30 11:00:00 -05001191 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001192 resp, content = self._responses[request_id]
1193 if resp['status'] == '401':
Joe Gregorio654f4a22012-02-09 14:15:44 -05001194 redo_order.append(request_id)
1195 request = self._requests[request_id]
1196 self._refresh_and_apply_credentials(request, http)
1197 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001198
Joe Gregorio654f4a22012-02-09 14:15:44 -05001199 if redo_requests:
1200 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001201
Joe Gregorio654f4a22012-02-09 14:15:44 -05001202 # Now process all callbacks that are erroring, and raise an exception for
1203 # ones that return a non-2xx response? Or add extra parameter to callback
1204 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001205
Joe Gregorio654f4a22012-02-09 14:15:44 -05001206 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001207 resp, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001208
Joe Gregorio654f4a22012-02-09 14:15:44 -05001209 request = self._requests[request_id]
1210 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001211
Joe Gregorio654f4a22012-02-09 14:15:44 -05001212 response = None
1213 exception = None
1214 try:
Joe Gregorio3fb93672012-07-25 11:31:11 -04001215 if resp.status >= 300:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001216 raise HttpError(resp, content, uri=request.uri)
Joe Gregorioc752e332012-07-11 14:43:52 -04001217 response = request.postproc(resp, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001218 except HttpError, e:
1219 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001220
Joe Gregorio654f4a22012-02-09 14:15:44 -05001221 if callback is not None:
1222 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001223 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001224 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001225
1226
Joe Gregorioaf276d22010-12-09 14:26:58 -05001227class HttpRequestMock(object):
1228 """Mock of HttpRequest.
1229
1230 Do not construct directly, instead use RequestMockBuilder.
1231 """
1232
1233 def __init__(self, resp, content, postproc):
1234 """Constructor for HttpRequestMock
1235
1236 Args:
1237 resp: httplib2.Response, the response to emulate coming from the request
1238 content: string, the response body
1239 postproc: callable, the post processing function usually supplied by
1240 the model class. See model.JsonModel.response() as an example.
1241 """
1242 self.resp = resp
1243 self.content = content
1244 self.postproc = postproc
1245 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001246 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001247 if 'reason' in self.resp:
1248 self.resp.reason = self.resp['reason']
1249
1250 def execute(self, http=None):
1251 """Execute the request.
1252
1253 Same behavior as HttpRequest.execute(), but the response is
1254 mocked and not really from an HTTP request/response.
1255 """
1256 return self.postproc(self.resp, self.content)
1257
1258
1259class RequestMockBuilder(object):
1260 """A simple mock of HttpRequest
1261
1262 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001263 tuples of (httplib2.Response, content, opt_expected_body) that should be
1264 returned when that method is called. None may also be passed in for the
1265 httplib2.Response, in which case a 200 OK response will be generated.
1266 If an opt_expected_body (str or dict) is provided, it will be compared to
1267 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001268
1269 Example:
1270 response = '{"data": {"id": "tag:google.c...'
1271 requestBuilder = RequestMockBuilder(
1272 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001273 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001274 }
1275 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001276 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001277
1278 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001279 200 OK with an empty string as the response content or raise an excpetion
1280 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001281 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001282
1283 For more details see the project wiki.
1284 """
1285
Joe Gregorioa388ce32011-09-09 17:19:13 -04001286 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001287 """Constructor for RequestMockBuilder
1288
1289 The constructed object should be a callable object
1290 that can replace the class HttpResponse.
1291
1292 responses - A dictionary that maps methodIds into tuples
1293 of (httplib2.Response, content). The methodId
1294 comes from the 'rpcName' field in the discovery
1295 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001296 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1297 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001298 """
1299 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001300 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001301
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001302 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001303 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001304 """Implements the callable interface that discovery.build() expects
1305 of requestBuilder, which is to build an object compatible with
1306 HttpRequest.execute(). See that method for the description of the
1307 parameters and the expected response.
1308 """
1309 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001310 response = self.responses[methodId]
1311 resp, content = response[:2]
1312 if len(response) > 2:
1313 # Test the body against the supplied expected_body.
1314 expected_body = response[2]
1315 if bool(expected_body) != bool(body):
1316 # Not expecting a body and provided one
1317 # or expecting a body and not provided one.
1318 raise UnexpectedBodyError(expected_body, body)
1319 if isinstance(expected_body, str):
1320 expected_body = simplejson.loads(expected_body)
1321 body = simplejson.loads(body)
1322 if body != expected_body:
1323 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001324 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001325 elif self.check_unexpected:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -04001326 raise UnexpectedMethodError(methodId=methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001327 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001328 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001329 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001330
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001331
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001332class HttpMock(object):
1333 """Mock of httplib2.Http"""
1334
Joe Gregorioec343652011-02-16 16:52:51 -05001335 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001336 """
1337 Args:
1338 filename: string, absolute filename to read response from
1339 headers: dict, header to return with response
1340 """
Joe Gregorioec343652011-02-16 16:52:51 -05001341 if headers is None:
1342 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001343 f = file(filename, 'r')
1344 self.data = f.read()
1345 f.close()
1346 self.headers = headers
1347
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001348 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001349 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001350 body=None,
1351 headers=None,
1352 redirections=1,
1353 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001354 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001355
1356
1357class HttpMockSequence(object):
1358 """Mock of httplib2.Http
1359
1360 Mocks a sequence of calls to request returning different responses for each
1361 call. Create an instance initialized with the desired response headers
1362 and content and then use as if an httplib2.Http instance.
1363
1364 http = HttpMockSequence([
1365 ({'status': '401'}, ''),
1366 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1367 ({'status': '200'}, 'echo_request_headers'),
1368 ])
1369 resp, content = http.request("http://examples.com")
1370
1371 There are special values you can pass in for content to trigger
1372 behavours that are helpful in testing.
1373
1374 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001375 'echo_request_headers_as_json' means return the request headers in
1376 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001377 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001378 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001379 """
1380
1381 def __init__(self, iterable):
1382 """
1383 Args:
1384 iterable: iterable, a sequence of pairs of (headers, body)
1385 """
1386 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001387 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001388
1389 def request(self, uri,
1390 method='GET',
1391 body=None,
1392 headers=None,
1393 redirections=1,
1394 connection_type=None):
1395 resp, content = self._iterable.pop(0)
1396 if content == 'echo_request_headers':
1397 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001398 elif content == 'echo_request_headers_as_json':
1399 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001400 elif content == 'echo_request_body':
1401 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001402 elif content == 'echo_request_uri':
1403 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001404 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001405
1406
1407def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001408 """Set the user-agent on every request.
1409
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001410 Args:
1411 http - An instance of httplib2.Http
1412 or something that acts like it.
1413 user_agent: string, the value for the user-agent header.
1414
1415 Returns:
1416 A modified instance of http that was passed in.
1417
1418 Example:
1419
1420 h = httplib2.Http()
1421 h = set_user_agent(h, "my-app-name/6.0")
1422
1423 Most of the time the user-agent will be set doing auth, this is for the rare
1424 cases where you are accessing an unauthenticated endpoint.
1425 """
1426 request_orig = http.request
1427
1428 # The closure that will replace 'httplib2.Http.request'.
1429 def new_request(uri, method='GET', body=None, headers=None,
1430 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1431 connection_type=None):
1432 """Modify the request headers to add the user-agent."""
1433 if headers is None:
1434 headers = {}
1435 if 'user-agent' in headers:
1436 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1437 else:
1438 headers['user-agent'] = user_agent
1439 resp, content = request_orig(uri, method, body, headers,
1440 redirections, connection_type)
1441 return resp, content
1442
1443 http.request = new_request
1444 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001445
1446
1447def tunnel_patch(http):
1448 """Tunnel PATCH requests over POST.
1449 Args:
1450 http - An instance of httplib2.Http
1451 or something that acts like it.
1452
1453 Returns:
1454 A modified instance of http that was passed in.
1455
1456 Example:
1457
1458 h = httplib2.Http()
1459 h = tunnel_patch(h, "my-app-name/6.0")
1460
1461 Useful if you are running on a platform that doesn't support PATCH.
1462 Apply this last if you are using OAuth 1.0, as changing the method
1463 will result in a different signature.
1464 """
1465 request_orig = http.request
1466
1467 # The closure that will replace 'httplib2.Http.request'.
1468 def new_request(uri, method='GET', body=None, headers=None,
1469 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1470 connection_type=None):
1471 """Modify the request headers to add the user-agent."""
1472 if headers is None:
1473 headers = {}
1474 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001475 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001476 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001477 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001478 headers['x-http-method-override'] = "PATCH"
1479 method = 'POST'
1480 resp, content = request_orig(uri, method, body, headers,
1481 redirections, connection_type)
1482 return resp, content
1483
1484 http.request = new_request
1485 return http