blob: 753dd09a0c936aef0a88eed735d88c0059301fd8 [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 Gregorio549230c2012-01-11 10:38:05 -050046from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040047
48
Joe Gregorio910b9b12012-06-12 09:36:30 -040049DEFAULT_CHUNK_SIZE = 512*1024
50
Joe Gregorioba5c7902012-08-03 12:48:16 -040051MAX_URI_LENGTH = 4000
52
Joe Gregorio910b9b12012-06-12 09:36:30 -040053
Joe Gregoriod0bd3882011-11-22 09:49:47 -050054class MediaUploadProgress(object):
55 """Status of a resumable upload."""
56
57 def __init__(self, resumable_progress, total_size):
58 """Constructor.
59
60 Args:
61 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040062 total_size: int, total bytes in complete upload, or None if the total
63 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050064 """
65 self.resumable_progress = resumable_progress
66 self.total_size = total_size
67
68 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040069 """Percent of upload completed, as a float.
70
71 Returns:
72 the percentage complete as a float, returning 0.0 if the total size of
73 the upload is unknown.
74 """
75 if self.total_size is not None:
76 return float(self.resumable_progress) / float(self.total_size)
77 else:
78 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050079
80
Joe Gregorio708388c2012-06-15 13:43:04 -040081class MediaDownloadProgress(object):
82 """Status of a resumable download."""
83
84 def __init__(self, resumable_progress, total_size):
85 """Constructor.
86
87 Args:
88 resumable_progress: int, bytes received so far.
89 total_size: int, total bytes in complete download.
90 """
91 self.resumable_progress = resumable_progress
92 self.total_size = total_size
93
94 def progress(self):
95 """Percent of download completed, as a float.
96
97 Returns:
98 the percentage complete as a float, returning 0.0 if the total size of
99 the download is unknown.
100 """
101 if self.total_size is not None:
102 return float(self.resumable_progress) / float(self.total_size)
103 else:
104 return 0.0
105
106
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500107class MediaUpload(object):
108 """Describes a media object to upload.
109
110 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400111
112 Note that subclasses of MediaUpload may allow you to control the chunksize
113 when upload a media object. It is important to keep the size of the chunk as
114 large as possible to keep the upload efficient. Other factors may influence
115 the size of the chunk you use, particularly if you are working in an
116 environment where individual HTTP requests may have a hardcoded time limit,
117 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500118 """
119
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500120 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400121 """Chunk size for resumable uploads.
122
123 Returns:
124 Chunk size in bytes.
125 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500126 raise NotImplementedError()
127
128 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400129 """Mime type of the body.
130
131 Returns:
132 Mime type.
133 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500134 return 'application/octet-stream'
135
Joe Gregorio910b9b12012-06-12 09:36:30 -0400136 def size(self):
137 """Size of upload.
138
139 Returns:
140 Size of the body, or None of the size is unknown.
141 """
142 return None
143
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500144 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400145 """Whether this upload is resumable.
146
147 Returns:
148 True if resumable upload or False.
149 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500150 return False
151
Joe Gregorio910b9b12012-06-12 09:36:30 -0400152 def getbytes(self, begin, end):
153 """Get bytes from the media.
154
155 Args:
156 begin: int, offset from beginning of file.
157 length: int, number of bytes to read, starting at begin.
158
159 Returns:
160 A string of bytes read. May be shorter than length if EOF was reached
161 first.
162 """
163 raise NotImplementedError()
164
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500165 def _to_json(self, strip=None):
166 """Utility function for creating a JSON representation of a MediaUpload.
167
168 Args:
169 strip: array, An array of names of members to not include in the JSON.
170
171 Returns:
172 string, a JSON representation of this instance, suitable to pass to
173 from_json().
174 """
175 t = type(self)
176 d = copy.copy(self.__dict__)
177 if strip is not None:
178 for member in strip:
179 del d[member]
180 d['_class'] = t.__name__
181 d['_module'] = t.__module__
182 return simplejson.dumps(d)
183
184 def to_json(self):
185 """Create a JSON representation of an instance of MediaUpload.
186
187 Returns:
188 string, a JSON representation of this instance, suitable to pass to
189 from_json().
190 """
191 return self._to_json()
192
193 @classmethod
194 def new_from_json(cls, s):
195 """Utility class method to instantiate a MediaUpload subclass from a JSON
196 representation produced by to_json().
197
198 Args:
199 s: string, JSON from to_json().
200
201 Returns:
202 An instance of the subclass of MediaUpload that was serialized with
203 to_json().
204 """
205 data = simplejson.loads(s)
206 # Find and call the right classmethod from_json() to restore the object.
207 module = data['_module']
208 m = __import__(module, fromlist=module.split('.')[:-1])
209 kls = getattr(m, data['_class'])
210 from_json = getattr(kls, 'from_json')
211 return from_json(s)
212
Joe Gregorio66f57522011-11-30 11:00:00 -0500213
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500214class MediaFileUpload(MediaUpload):
215 """A MediaUpload for a file.
216
217 Construct a MediaFileUpload and pass as the media_body parameter of the
218 method. For example, if we had a service that allowed uploading images:
219
220
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400221 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400222 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400223 farm.animals()..insert(
224 id='cow',
225 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500226 media_body=media).execute()
227 """
228
Joe Gregorio910b9b12012-06-12 09:36:30 -0400229 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500230 """Constructor.
231
232 Args:
233 filename: string, Name of the file.
234 mimetype: string, Mime-type of the file. If None then a mime-type will be
235 guessed from the file extension.
236 chunksize: int, File will be uploaded in chunks of this many bytes. Only
237 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500238 resumable: bool, True if this is a resumable upload. False means upload
239 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500240 """
241 self._filename = filename
242 self._size = os.path.getsize(filename)
243 self._fd = None
244 if mimetype is None:
245 (mimetype, encoding) = mimetypes.guess_type(filename)
246 self._mimetype = mimetype
247 self._chunksize = chunksize
248 self._resumable = resumable
249
Joe Gregorio910b9b12012-06-12 09:36:30 -0400250 def chunksize(self):
251 """Chunk size for resumable uploads.
252
253 Returns:
254 Chunk size in bytes.
255 """
256 return self._chunksize
257
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500258 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400259 """Mime type of the body.
260
261 Returns:
262 Mime type.
263 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500264 return self._mimetype
265
266 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400267 """Size of upload.
268
269 Returns:
270 Size of the body, or None of the size is unknown.
271 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500272 return self._size
273
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500274 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400275 """Whether this upload is resumable.
276
277 Returns:
278 True if resumable upload or False.
279 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500280 return self._resumable
281
282 def getbytes(self, begin, length):
283 """Get bytes from the media.
284
285 Args:
286 begin: int, offset from beginning of file.
287 length: int, number of bytes to read, starting at begin.
288
289 Returns:
290 A string of bytes read. May be shorted than length if EOF was reached
291 first.
292 """
293 if self._fd is None:
294 self._fd = open(self._filename, 'rb')
295 self._fd.seek(begin)
296 return self._fd.read(length)
297
298 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400299 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500300
301 Returns:
302 string, a JSON representation of this instance, suitable to pass to
303 from_json().
304 """
305 return self._to_json(['_fd'])
306
307 @staticmethod
308 def from_json(s):
309 d = simplejson.loads(s)
310 return MediaFileUpload(
311 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
312
313
Joe Gregorio910b9b12012-06-12 09:36:30 -0400314class MediaIoBaseUpload(MediaUpload):
315 """A MediaUpload for a io.Base objects.
316
317 Note that the Python file object is compatible with io.Base and can be used
318 with this class also.
319
Joe Gregorio910b9b12012-06-12 09:36:30 -0400320 fh = io.BytesIO('...Some data to upload...')
321 media = MediaIoBaseUpload(fh, mimetype='image/png',
322 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400323 farm.animals().insert(
324 id='cow',
325 name='cow.png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400326 media_body=media).execute()
327 """
328
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400329 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400330 resumable=False):
331 """Constructor.
332
333 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400334 fd: io.Base or file object, The source of the bytes to upload. MUST be
Joe Gregorio44454e42012-06-15 08:38:53 -0400335 opened in blocking mode, do not use streams opened in non-blocking mode.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400336 mimetype: string, Mime-type of the file. If None then a mime-type will be
337 guessed from the file extension.
338 chunksize: int, File will be uploaded in chunks of this many bytes. Only
339 used if resumable=True.
340 resumable: bool, True if this is a resumable upload. False means upload
341 in a single request.
342 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400343 self._fd = fd
Joe Gregorio910b9b12012-06-12 09:36:30 -0400344 self._mimetype = mimetype
345 self._chunksize = chunksize
346 self._resumable = resumable
347 self._size = None
348 try:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400349 if hasattr(self._fd, 'fileno'):
350 fileno = self._fd.fileno()
Joe Gregorio44454e42012-06-15 08:38:53 -0400351
352 # Pipes and such show up as 0 length files.
353 size = os.fstat(fileno).st_size
354 if size:
355 self._size = os.fstat(fileno).st_size
Joe Gregorio910b9b12012-06-12 09:36:30 -0400356 except IOError:
357 pass
358
359 def chunksize(self):
360 """Chunk size for resumable uploads.
361
362 Returns:
363 Chunk size in bytes.
364 """
365 return self._chunksize
366
367 def mimetype(self):
368 """Mime type of the body.
369
370 Returns:
371 Mime type.
372 """
373 return self._mimetype
374
375 def size(self):
376 """Size of upload.
377
378 Returns:
379 Size of the body, or None of the size is unknown.
380 """
381 return self._size
382
383 def resumable(self):
384 """Whether this upload is resumable.
385
386 Returns:
387 True if resumable upload or False.
388 """
389 return self._resumable
390
391 def getbytes(self, begin, length):
392 """Get bytes from the media.
393
394 Args:
395 begin: int, offset from beginning of file.
396 length: int, number of bytes to read, starting at begin.
397
398 Returns:
399 A string of bytes read. May be shorted than length if EOF was reached
400 first.
401 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400402 self._fd.seek(begin)
403 return self._fd.read(length)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400404
405 def to_json(self):
406 """This upload type is not serializable."""
407 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
408
409
Ali Afshar6f11ea12012-02-07 10:32:14 -0500410class MediaInMemoryUpload(MediaUpload):
411 """MediaUpload for a chunk of bytes.
412
413 Construct a MediaFileUpload and pass as the media_body parameter of the
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400414 method.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500415 """
416
417 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400418 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500419 """Create a new MediaBytesUpload.
420
421 Args:
422 body: string, Bytes of body content.
423 mimetype: string, Mime-type of the file or default of
424 'application/octet-stream'.
425 chunksize: int, File will be uploaded in chunks of this many bytes. Only
426 used if resumable=True.
427 resumable: bool, True if this is a resumable upload. False means upload
428 in a single request.
429 """
430 self._body = body
431 self._mimetype = mimetype
432 self._resumable = resumable
433 self._chunksize = chunksize
434
435 def chunksize(self):
436 """Chunk size for resumable uploads.
437
438 Returns:
439 Chunk size in bytes.
440 """
441 return self._chunksize
442
443 def mimetype(self):
444 """Mime type of the body.
445
446 Returns:
447 Mime type.
448 """
449 return self._mimetype
450
451 def size(self):
452 """Size of upload.
453
454 Returns:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400455 Size of the body, or None of the size is unknown.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500456 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400457 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500458
459 def resumable(self):
460 """Whether this upload is resumable.
461
462 Returns:
463 True if resumable upload or False.
464 """
465 return self._resumable
466
467 def getbytes(self, begin, length):
468 """Get bytes from the media.
469
470 Args:
471 begin: int, offset from beginning of file.
472 length: int, number of bytes to read, starting at begin.
473
474 Returns:
475 A string of bytes read. May be shorter than length if EOF was reached
476 first.
477 """
478 return self._body[begin:begin + length]
479
480 def to_json(self):
481 """Create a JSON representation of a MediaInMemoryUpload.
482
483 Returns:
484 string, a JSON representation of this instance, suitable to pass to
485 from_json().
486 """
487 t = type(self)
488 d = copy.copy(self.__dict__)
489 del d['_body']
490 d['_class'] = t.__name__
491 d['_module'] = t.__module__
492 d['_b64body'] = base64.b64encode(self._body)
493 return simplejson.dumps(d)
494
495 @staticmethod
496 def from_json(s):
497 d = simplejson.loads(s)
498 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
499 d['_mimetype'], d['_chunksize'],
500 d['_resumable'])
501
502
Joe Gregorio708388c2012-06-15 13:43:04 -0400503class MediaIoBaseDownload(object):
504 """"Download media resources.
505
506 Note that the Python file object is compatible with io.Base and can be used
507 with this class also.
508
509
510 Example:
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400511 request = farms.animals().get_media(id='cow')
512 fh = io.FileIO('cow.png', mode='wb')
Joe Gregorio708388c2012-06-15 13:43:04 -0400513 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
514
515 done = False
516 while done is False:
517 status, done = downloader.next_chunk()
518 if status:
519 print "Download %d%%." % int(status.progress() * 100)
520 print "Download Complete!"
521 """
522
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400523 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
Joe Gregorio708388c2012-06-15 13:43:04 -0400524 """Constructor.
525
526 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400527 fd: io.Base or file object, The stream in which to write the downloaded
Joe Gregorio708388c2012-06-15 13:43:04 -0400528 bytes.
529 request: apiclient.http.HttpRequest, the media request to perform in
530 chunks.
531 chunksize: int, File will be downloaded in chunks of this many bytes.
532 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400533 self._fd = fd
534 self._request = request
535 self._uri = request.uri
536 self._chunksize = chunksize
537 self._progress = 0
538 self._total_size = None
539 self._done = False
Joe Gregorio708388c2012-06-15 13:43:04 -0400540
541 def next_chunk(self):
542 """Get the next chunk of the download.
543
544 Returns:
545 (status, done): (MediaDownloadStatus, boolean)
546 The value of 'done' will be True when the media has been fully
547 downloaded.
548
549 Raises:
550 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400551 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio708388c2012-06-15 13:43:04 -0400552 """
553 headers = {
554 'range': 'bytes=%d-%d' % (
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400555 self._progress, self._progress + self._chunksize)
Joe Gregorio708388c2012-06-15 13:43:04 -0400556 }
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400557 http = self._request.http
Joe Gregorio708388c2012-06-15 13:43:04 -0400558 http.follow_redirects = False
559
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400560 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400561 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400562 self._uri = resp['location']
563 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400564 if resp.status in [200, 206]:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400565 self._progress += len(content)
566 self._fd.write(content)
Joe Gregorio708388c2012-06-15 13:43:04 -0400567
568 if 'content-range' in resp:
569 content_range = resp['content-range']
570 length = content_range.rsplit('/', 1)[1]
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400571 self._total_size = int(length)
Joe Gregorio708388c2012-06-15 13:43:04 -0400572
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400573 if self._progress == self._total_size:
574 self._done = True
575 return MediaDownloadProgress(self._progress, self._total_size), self._done
Joe Gregorio708388c2012-06-15 13:43:04 -0400576 else:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400577 raise HttpError(resp, content, self._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400578
579
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400580class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500581 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400582
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500583 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500584 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500585 body=None,
586 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500587 methodId=None,
588 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500589 """Constructor for an HttpRequest.
590
Joe Gregorioaf276d22010-12-09 14:26:58 -0500591 Args:
592 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500593 postproc: callable, called on the HTTP response and content to transform
594 it into a data object before returning, or raising an exception
595 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500596 uri: string, the absolute URI to send the request to
597 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500598 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500599 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500600 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500601 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500602 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400603 self.uri = uri
604 self.method = method
605 self.body = body
606 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500607 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400608 self.http = http
609 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500610 self.resumable = resumable
Joe Gregorio910b9b12012-06-12 09:36:30 -0400611 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500612
Joe Gregorio66f57522011-11-30 11:00:00 -0500613 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500614 major, minor, params = mimeparse.parse_mime_type(
615 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500616
Joe Gregorio945be3e2012-01-27 17:01:06 -0500617 # The size of the non-media part of the request.
618 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500619
620 # The resumable URI to send chunks to.
621 self.resumable_uri = None
622
623 # The bytes that have been uploaded.
624 self.resumable_progress = 0
625
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400626 def execute(self, http=None):
627 """Execute the request.
628
Joe Gregorioaf276d22010-12-09 14:26:58 -0500629 Args:
630 http: httplib2.Http, an http object to be used in place of the
631 one the HttpRequest request object was constructed with.
632
633 Returns:
634 A deserialized object model of the response body as determined
635 by the postproc.
636
637 Raises:
638 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400639 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400640 """
641 if http is None:
642 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500643 if self.resumable:
644 body = None
645 while body is None:
646 _, body = self.next_chunk(http)
647 return body
648 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500649 if 'content-length' not in self.headers:
650 self.headers['content-length'] = str(self.body_size)
Joe Gregorioba5c7902012-08-03 12:48:16 -0400651 # If the request URI is too long then turn it into a POST request.
652 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
653 self.method = 'POST'
654 self.headers['x-http-method-override'] = 'GET'
655 self.headers['content-type'] = 'application/x-www-form-urlencoded'
656 parsed = urlparse.urlparse(self.uri)
657 self.uri = urlparse.urlunparse(
658 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
659 None)
660 )
661 self.body = parsed.query
662 self.headers['content-length'] = str(len(self.body))
663
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500664 resp, content = http.request(self.uri, self.method,
665 body=self.body,
666 headers=self.headers)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500667 if resp.status >= 300:
668 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400669 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500670
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500671 def next_chunk(self, http=None):
672 """Execute the next step of a resumable upload.
673
Joe Gregorio66f57522011-11-30 11:00:00 -0500674 Can only be used if the method being executed supports media uploads and
675 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500676
677 Example:
678
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400679 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio66f57522011-11-30 11:00:00 -0500680 chunksize=1000, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400681 request = farm.animals().insert(
682 id='cow',
683 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500684 media_body=media)
685
686 response = None
687 while response is None:
688 status, response = request.next_chunk()
689 if status:
690 print "Upload %d%% complete." % int(status.progress() * 100)
691
692
693 Returns:
694 (status, body): (ResumableMediaStatus, object)
695 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400696
697 Raises:
698 apiclient.errors.HttpError if the response was not a 2xx.
Joe Gregorio77af30a2012-08-01 14:54:40 -0400699 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500700 """
701 if http is None:
702 http = self.http
703
Joe Gregorio910b9b12012-06-12 09:36:30 -0400704 if self.resumable.size() is None:
705 size = '*'
706 else:
707 size = str(self.resumable.size())
708
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500709 if self.resumable_uri is None:
710 start_headers = copy.copy(self.headers)
711 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400712 if size != '*':
713 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500714 start_headers['content-length'] = str(self.body_size)
715
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500716 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500717 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500718 headers=start_headers)
719 if resp.status == 200 and 'location' in resp:
720 self.resumable_uri = resp['location']
721 else:
722 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregorio910b9b12012-06-12 09:36:30 -0400723 elif self._in_error_state:
724 # If we are in an error state then query the server for current state of
725 # the upload by sending an empty PUT and reading the 'range' header in
726 # the response.
727 headers = {
728 'Content-Range': 'bytes */%s' % size,
729 'content-length': '0'
730 }
731 resp, content = http.request(self.resumable_uri, 'PUT',
732 headers=headers)
733 status, body = self._process_response(resp, content)
734 if body:
735 # The upload was complete.
736 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500737
Joe Gregorio910b9b12012-06-12 09:36:30 -0400738 data = self.resumable.getbytes(
739 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400740
741 # A short read implies that we are at EOF, so finish the upload.
742 if len(data) < self.resumable.chunksize():
743 size = str(self.resumable_progress + len(data))
744
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500745 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400746 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500747 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400748 size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500749 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400750 try:
751 resp, content = http.request(self.resumable_uri, 'PUT',
752 body=data,
753 headers=headers)
754 except:
755 self._in_error_state = True
756 raise
757
758 return self._process_response(resp, content)
759
760 def _process_response(self, resp, content):
761 """Process the response from a single chunk upload.
762
763 Args:
764 resp: httplib2.Response, the response object.
765 content: string, the content of the response.
766
767 Returns:
768 (status, body): (ResumableMediaStatus, object)
769 The body will be None until the resumable media is fully uploaded.
770
771 Raises:
772 apiclient.errors.HttpError if the response was not a 2xx or a 308.
773 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500774 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400775 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500776 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500777 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400778 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500779 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500780 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500781 if 'location' in resp:
782 self.resumable_uri = resp['location']
783 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400784 self._in_error_state = True
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500785 raise HttpError(resp, content, self.uri)
786
Joe Gregorio945be3e2012-01-27 17:01:06 -0500787 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
788 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500789
790 def to_json(self):
791 """Returns a JSON representation of the HttpRequest."""
792 d = copy.copy(self.__dict__)
793 if d['resumable'] is not None:
794 d['resumable'] = self.resumable.to_json()
795 del d['http']
796 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400797
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500798 return simplejson.dumps(d)
799
800 @staticmethod
801 def from_json(s, http, postproc):
802 """Returns an HttpRequest populated with info from a JSON object."""
803 d = simplejson.loads(s)
804 if d['resumable'] is not None:
805 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
806 return HttpRequest(
807 http,
808 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500809 uri=d['uri'],
810 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500811 body=d['body'],
812 headers=d['headers'],
813 methodId=d['methodId'],
814 resumable=d['resumable'])
815
Joe Gregorioaf276d22010-12-09 14:26:58 -0500816
Joe Gregorio66f57522011-11-30 11:00:00 -0500817class BatchHttpRequest(object):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400818 """Batches multiple HttpRequest objects into a single HTTP request.
819
820 Example:
821 from apiclient.http import BatchHttpRequest
822
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400823 def list_animals(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400824 \"\"\"Do something with the animals list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400825 if exception is not None:
826 # Do something with the exception.
827 pass
828 else:
829 # Do something with the response.
830 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400831
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400832 def list_farmers(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400833 \"\"\"Do something with the farmers list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400834 if exception is not None:
835 # Do something with the exception.
836 pass
837 else:
838 # Do something with the response.
839 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400840
841 service = build('farm', 'v2')
842
843 batch = BatchHttpRequest()
844
845 batch.add(service.animals().list(), list_animals)
846 batch.add(service.farmers().list(), list_farmers)
847 batch.execute(http)
848 """
Joe Gregorio66f57522011-11-30 11:00:00 -0500849
850 def __init__(self, callback=None, batch_uri=None):
851 """Constructor for a BatchHttpRequest.
852
853 Args:
854 callback: callable, A callback to be called for each response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -0400855 form callback(id, response, exception). The first parameter is the
856 request id, and the second is the deserialized response object. The
857 third is an apiclient.errors.HttpError exception object if an HTTP error
858 occurred while processing the request, or None if no error occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -0500859 batch_uri: string, URI to send batch requests to.
860 """
861 if batch_uri is None:
862 batch_uri = 'https://www.googleapis.com/batch'
863 self._batch_uri = batch_uri
864
865 # Global callback to be called for each individual response in the batch.
866 self._callback = callback
867
Joe Gregorio654f4a22012-02-09 14:15:44 -0500868 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500869 self._requests = {}
870
Joe Gregorio654f4a22012-02-09 14:15:44 -0500871 # A map from id to callback.
872 self._callbacks = {}
873
Joe Gregorio66f57522011-11-30 11:00:00 -0500874 # List of request ids, in the order in which they were added.
875 self._order = []
876
877 # The last auto generated id.
878 self._last_auto_id = 0
879
880 # Unique ID on which to base the Content-ID headers.
881 self._base_id = None
882
Joe Gregorioc752e332012-07-11 14:43:52 -0400883 # A map from request id to (httplib2.Response, content) response pairs
Joe Gregorio654f4a22012-02-09 14:15:44 -0500884 self._responses = {}
885
886 # A map of id(Credentials) that have been refreshed.
887 self._refreshed_credentials = {}
888
889 def _refresh_and_apply_credentials(self, request, http):
890 """Refresh the credentials and apply to the request.
891
892 Args:
893 request: HttpRequest, the request.
894 http: httplib2.Http, the global http object for the batch.
895 """
896 # For the credentials to refresh, but only once per refresh_token
897 # If there is no http per the request then refresh the http passed in
898 # via execute()
899 creds = None
900 if request.http is not None and hasattr(request.http.request,
901 'credentials'):
902 creds = request.http.request.credentials
903 elif http is not None and hasattr(http.request, 'credentials'):
904 creds = http.request.credentials
905 if creds is not None:
906 if id(creds) not in self._refreshed_credentials:
907 creds.refresh(http)
908 self._refreshed_credentials[id(creds)] = 1
909
910 # Only apply the credentials if we are using the http object passed in,
911 # otherwise apply() will get called during _serialize_request().
912 if request.http is None or not hasattr(request.http.request,
913 'credentials'):
914 creds.apply(request.headers)
915
Joe Gregorio66f57522011-11-30 11:00:00 -0500916 def _id_to_header(self, id_):
917 """Convert an id to a Content-ID header value.
918
919 Args:
920 id_: string, identifier of individual request.
921
922 Returns:
923 A Content-ID header with the id_ encoded into it. A UUID is prepended to
924 the value because Content-ID headers are supposed to be universally
925 unique.
926 """
927 if self._base_id is None:
928 self._base_id = uuid.uuid4()
929
930 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
931
932 def _header_to_id(self, header):
933 """Convert a Content-ID header value to an id.
934
935 Presumes the Content-ID header conforms to the format that _id_to_header()
936 returns.
937
938 Args:
939 header: string, Content-ID header value.
940
941 Returns:
942 The extracted id value.
943
944 Raises:
945 BatchError if the header is not in the expected format.
946 """
947 if header[0] != '<' or header[-1] != '>':
948 raise BatchError("Invalid value for Content-ID: %s" % header)
949 if '+' not in header:
950 raise BatchError("Invalid value for Content-ID: %s" % header)
951 base, id_ = header[1:-1].rsplit('+', 1)
952
953 return urllib.unquote(id_)
954
955 def _serialize_request(self, request):
956 """Convert an HttpRequest object into a string.
957
958 Args:
959 request: HttpRequest, the request to serialize.
960
961 Returns:
962 The request as a string in application/http format.
963 """
964 # Construct status line
965 parsed = urlparse.urlparse(request.uri)
966 request_line = urlparse.urlunparse(
967 (None, None, parsed.path, parsed.params, parsed.query, None)
968 )
969 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500970 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500971 msg = MIMENonMultipart(major, minor)
972 headers = request.headers.copy()
973
Joe Gregorio654f4a22012-02-09 14:15:44 -0500974 if request.http is not None and hasattr(request.http.request,
975 'credentials'):
976 request.http.request.credentials.apply(headers)
977
Joe Gregorio66f57522011-11-30 11:00:00 -0500978 # MIMENonMultipart adds its own Content-Type header.
979 if 'content-type' in headers:
980 del headers['content-type']
981
982 for key, value in headers.iteritems():
983 msg[key] = value
984 msg['Host'] = parsed.netloc
985 msg.set_unixfrom(None)
986
987 if request.body is not None:
988 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500989 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500990
Joe Gregorio654f4a22012-02-09 14:15:44 -0500991 # Serialize the mime message.
992 fp = StringIO.StringIO()
993 # maxheaderlen=0 means don't line wrap headers.
994 g = Generator(fp, maxheaderlen=0)
995 g.flatten(msg, unixfrom=False)
996 body = fp.getvalue()
997
Joe Gregorio66f57522011-11-30 11:00:00 -0500998 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
999 if request.body is None:
1000 body = body[:-2]
1001
Joe Gregoriodd813822012-01-25 10:32:47 -05001002 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -05001003
1004 def _deserialize_response(self, payload):
1005 """Convert string into httplib2 response and content.
1006
1007 Args:
1008 payload: string, headers and body as a string.
1009
1010 Returns:
Joe Gregorioc752e332012-07-11 14:43:52 -04001011 A pair (resp, content), such as would be returned from httplib2.request.
Joe Gregorio66f57522011-11-30 11:00:00 -05001012 """
1013 # Strip off the status line
1014 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001015 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001016
1017 # Parse the rest of the response
1018 parser = FeedParser()
1019 parser.feed(payload)
1020 msg = parser.close()
1021 msg['status'] = status
1022
1023 # Create httplib2.Response from the parsed headers.
1024 resp = httplib2.Response(msg)
1025 resp.reason = reason
1026 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1027
1028 content = payload.split('\r\n\r\n', 1)[1]
1029
1030 return resp, content
1031
1032 def _new_id(self):
1033 """Create a new id.
1034
1035 Auto incrementing number that avoids conflicts with ids already used.
1036
1037 Returns:
1038 string, a new unique id.
1039 """
1040 self._last_auto_id += 1
1041 while str(self._last_auto_id) in self._requests:
1042 self._last_auto_id += 1
1043 return str(self._last_auto_id)
1044
1045 def add(self, request, callback=None, request_id=None):
1046 """Add a new request.
1047
1048 Every callback added will be paired with a unique id, the request_id. That
1049 unique id will be passed back to the callback when the response comes back
1050 from the server. The default behavior is to have the library generate it's
1051 own unique id. If the caller passes in a request_id then they must ensure
1052 uniqueness for each request_id, and if they are not an exception is
1053 raised. Callers should either supply all request_ids or nevery supply a
1054 request id, to avoid such an error.
1055
1056 Args:
1057 request: HttpRequest, Request to add to the batch.
1058 callback: callable, A callback to be called for this response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -04001059 form callback(id, response, exception). The first parameter is the
1060 request id, and the second is the deserialized response object. The
1061 third is an apiclient.errors.HttpError exception object if an HTTP error
1062 occurred while processing the request, or None if no errors occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -05001063 request_id: string, A unique id for the request. The id will be passed to
1064 the callback with the response.
1065
1066 Returns:
1067 None
1068
1069 Raises:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001070 BatchError if a media request is added to a batch.
Joe Gregorio66f57522011-11-30 11:00:00 -05001071 KeyError is the request_id is not unique.
1072 """
1073 if request_id is None:
1074 request_id = self._new_id()
1075 if request.resumable is not None:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001076 raise BatchError("Media requests cannot be used in a batch request.")
Joe Gregorio66f57522011-11-30 11:00:00 -05001077 if request_id in self._requests:
1078 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001079 self._requests[request_id] = request
1080 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001081 self._order.append(request_id)
1082
Joe Gregorio654f4a22012-02-09 14:15:44 -05001083 def _execute(self, http, order, requests):
1084 """Serialize batch request, send to server, process response.
1085
1086 Args:
1087 http: httplib2.Http, an http object to be used to make the request with.
1088 order: list, list of request ids in the order they were added to the
1089 batch.
1090 request: list, list of request objects to send.
1091
1092 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001093 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio654f4a22012-02-09 14:15:44 -05001094 apiclient.errors.BatchError if the response is the wrong format.
1095 """
1096 message = MIMEMultipart('mixed')
1097 # Message should not write out it's own headers.
1098 setattr(message, '_write_headers', lambda self: None)
1099
1100 # Add all the individual requests.
1101 for request_id in order:
1102 request = requests[request_id]
1103
1104 msg = MIMENonMultipart('application', 'http')
1105 msg['Content-Transfer-Encoding'] = 'binary'
1106 msg['Content-ID'] = self._id_to_header(request_id)
1107
1108 body = self._serialize_request(request)
1109 msg.set_payload(body)
1110 message.attach(msg)
1111
1112 body = message.as_string()
1113
1114 headers = {}
1115 headers['content-type'] = ('multipart/mixed; '
1116 'boundary="%s"') % message.get_boundary()
1117
1118 resp, content = http.request(self._batch_uri, 'POST', body=body,
1119 headers=headers)
1120
1121 if resp.status >= 300:
1122 raise HttpError(resp, content, self._batch_uri)
1123
1124 # Now break out the individual responses and store each one.
1125 boundary, _ = content.split(None, 1)
1126
1127 # Prepend with a content-type header so FeedParser can handle it.
1128 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1129 for_parser = header + content
1130
1131 parser = FeedParser()
1132 parser.feed(for_parser)
1133 mime_response = parser.close()
1134
1135 if not mime_response.is_multipart():
1136 raise BatchError("Response not in multipart/mixed format.", resp,
1137 content)
1138
1139 for part in mime_response.get_payload():
1140 request_id = self._header_to_id(part['Content-ID'])
Joe Gregorioc752e332012-07-11 14:43:52 -04001141 response, content = self._deserialize_response(part.get_payload())
1142 self._responses[request_id] = (response, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001143
Joe Gregorio66f57522011-11-30 11:00:00 -05001144 def execute(self, http=None):
1145 """Execute all the requests as a single batched HTTP request.
1146
1147 Args:
1148 http: httplib2.Http, an http object to be used in place of the one the
1149 HttpRequest request object was constructed with. If one isn't supplied
1150 then use a http object from the requests in this batch.
1151
1152 Returns:
1153 None
1154
1155 Raises:
Joe Gregorio77af30a2012-08-01 14:54:40 -04001156 httplib2.HttpLib2Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001157 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001158 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001159
1160 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001161 if http is None:
1162 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001163 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001164 if request is not None:
1165 http = request.http
1166 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001167
Joe Gregorio66f57522011-11-30 11:00:00 -05001168 if http is None:
1169 raise ValueError("Missing a valid http object.")
1170
Joe Gregorio654f4a22012-02-09 14:15:44 -05001171 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001172
Joe Gregorio654f4a22012-02-09 14:15:44 -05001173 # Loop over all the requests and check for 401s. For each 401 request the
1174 # credentials should be refreshed and then sent again in a separate batch.
1175 redo_requests = {}
1176 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001177
Joe Gregorio66f57522011-11-30 11:00:00 -05001178 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001179 resp, content = self._responses[request_id]
1180 if resp['status'] == '401':
Joe Gregorio654f4a22012-02-09 14:15:44 -05001181 redo_order.append(request_id)
1182 request = self._requests[request_id]
1183 self._refresh_and_apply_credentials(request, http)
1184 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001185
Joe Gregorio654f4a22012-02-09 14:15:44 -05001186 if redo_requests:
1187 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001188
Joe Gregorio654f4a22012-02-09 14:15:44 -05001189 # Now process all callbacks that are erroring, and raise an exception for
1190 # ones that return a non-2xx response? Or add extra parameter to callback
1191 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001192
Joe Gregorio654f4a22012-02-09 14:15:44 -05001193 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001194 resp, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001195
Joe Gregorio654f4a22012-02-09 14:15:44 -05001196 request = self._requests[request_id]
1197 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001198
Joe Gregorio654f4a22012-02-09 14:15:44 -05001199 response = None
1200 exception = None
1201 try:
Joe Gregorio3fb93672012-07-25 11:31:11 -04001202 if resp.status >= 300:
1203 raise HttpError(resp, content, request.uri)
Joe Gregorioc752e332012-07-11 14:43:52 -04001204 response = request.postproc(resp, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001205 except HttpError, e:
1206 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001207
Joe Gregorio654f4a22012-02-09 14:15:44 -05001208 if callback is not None:
1209 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001210 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001211 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001212
1213
Joe Gregorioaf276d22010-12-09 14:26:58 -05001214class HttpRequestMock(object):
1215 """Mock of HttpRequest.
1216
1217 Do not construct directly, instead use RequestMockBuilder.
1218 """
1219
1220 def __init__(self, resp, content, postproc):
1221 """Constructor for HttpRequestMock
1222
1223 Args:
1224 resp: httplib2.Response, the response to emulate coming from the request
1225 content: string, the response body
1226 postproc: callable, the post processing function usually supplied by
1227 the model class. See model.JsonModel.response() as an example.
1228 """
1229 self.resp = resp
1230 self.content = content
1231 self.postproc = postproc
1232 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001233 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001234 if 'reason' in self.resp:
1235 self.resp.reason = self.resp['reason']
1236
1237 def execute(self, http=None):
1238 """Execute the request.
1239
1240 Same behavior as HttpRequest.execute(), but the response is
1241 mocked and not really from an HTTP request/response.
1242 """
1243 return self.postproc(self.resp, self.content)
1244
1245
1246class RequestMockBuilder(object):
1247 """A simple mock of HttpRequest
1248
1249 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001250 tuples of (httplib2.Response, content, opt_expected_body) that should be
1251 returned when that method is called. None may also be passed in for the
1252 httplib2.Response, in which case a 200 OK response will be generated.
1253 If an opt_expected_body (str or dict) is provided, it will be compared to
1254 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001255
1256 Example:
1257 response = '{"data": {"id": "tag:google.c...'
1258 requestBuilder = RequestMockBuilder(
1259 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001260 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001261 }
1262 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001263 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001264
1265 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001266 200 OK with an empty string as the response content or raise an excpetion
1267 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001268 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001269
1270 For more details see the project wiki.
1271 """
1272
Joe Gregorioa388ce32011-09-09 17:19:13 -04001273 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001274 """Constructor for RequestMockBuilder
1275
1276 The constructed object should be a callable object
1277 that can replace the class HttpResponse.
1278
1279 responses - A dictionary that maps methodIds into tuples
1280 of (httplib2.Response, content). The methodId
1281 comes from the 'rpcName' field in the discovery
1282 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001283 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1284 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001285 """
1286 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001287 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001288
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001289 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001290 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001291 """Implements the callable interface that discovery.build() expects
1292 of requestBuilder, which is to build an object compatible with
1293 HttpRequest.execute(). See that method for the description of the
1294 parameters and the expected response.
1295 """
1296 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001297 response = self.responses[methodId]
1298 resp, content = response[:2]
1299 if len(response) > 2:
1300 # Test the body against the supplied expected_body.
1301 expected_body = response[2]
1302 if bool(expected_body) != bool(body):
1303 # Not expecting a body and provided one
1304 # or expecting a body and not provided one.
1305 raise UnexpectedBodyError(expected_body, body)
1306 if isinstance(expected_body, str):
1307 expected_body = simplejson.loads(expected_body)
1308 body = simplejson.loads(body)
1309 if body != expected_body:
1310 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001311 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001312 elif self.check_unexpected:
1313 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001314 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001315 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001316 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001317
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001318
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001319class HttpMock(object):
1320 """Mock of httplib2.Http"""
1321
Joe Gregorioec343652011-02-16 16:52:51 -05001322 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001323 """
1324 Args:
1325 filename: string, absolute filename to read response from
1326 headers: dict, header to return with response
1327 """
Joe Gregorioec343652011-02-16 16:52:51 -05001328 if headers is None:
1329 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001330 f = file(filename, 'r')
1331 self.data = f.read()
1332 f.close()
1333 self.headers = headers
1334
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001335 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001336 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001337 body=None,
1338 headers=None,
1339 redirections=1,
1340 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001341 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001342
1343
1344class HttpMockSequence(object):
1345 """Mock of httplib2.Http
1346
1347 Mocks a sequence of calls to request returning different responses for each
1348 call. Create an instance initialized with the desired response headers
1349 and content and then use as if an httplib2.Http instance.
1350
1351 http = HttpMockSequence([
1352 ({'status': '401'}, ''),
1353 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1354 ({'status': '200'}, 'echo_request_headers'),
1355 ])
1356 resp, content = http.request("http://examples.com")
1357
1358 There are special values you can pass in for content to trigger
1359 behavours that are helpful in testing.
1360
1361 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001362 'echo_request_headers_as_json' means return the request headers in
1363 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001364 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001365 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001366 """
1367
1368 def __init__(self, iterable):
1369 """
1370 Args:
1371 iterable: iterable, a sequence of pairs of (headers, body)
1372 """
1373 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001374 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001375
1376 def request(self, uri,
1377 method='GET',
1378 body=None,
1379 headers=None,
1380 redirections=1,
1381 connection_type=None):
1382 resp, content = self._iterable.pop(0)
1383 if content == 'echo_request_headers':
1384 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001385 elif content == 'echo_request_headers_as_json':
1386 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001387 elif content == 'echo_request_body':
1388 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001389 elif content == 'echo_request_uri':
1390 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001391 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001392
1393
1394def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001395 """Set the user-agent on every request.
1396
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001397 Args:
1398 http - An instance of httplib2.Http
1399 or something that acts like it.
1400 user_agent: string, the value for the user-agent header.
1401
1402 Returns:
1403 A modified instance of http that was passed in.
1404
1405 Example:
1406
1407 h = httplib2.Http()
1408 h = set_user_agent(h, "my-app-name/6.0")
1409
1410 Most of the time the user-agent will be set doing auth, this is for the rare
1411 cases where you are accessing an unauthenticated endpoint.
1412 """
1413 request_orig = http.request
1414
1415 # The closure that will replace 'httplib2.Http.request'.
1416 def new_request(uri, method='GET', body=None, headers=None,
1417 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1418 connection_type=None):
1419 """Modify the request headers to add the user-agent."""
1420 if headers is None:
1421 headers = {}
1422 if 'user-agent' in headers:
1423 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1424 else:
1425 headers['user-agent'] = user_agent
1426 resp, content = request_orig(uri, method, body, headers,
1427 redirections, connection_type)
1428 return resp, content
1429
1430 http.request = new_request
1431 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001432
1433
1434def tunnel_patch(http):
1435 """Tunnel PATCH requests over POST.
1436 Args:
1437 http - An instance of httplib2.Http
1438 or something that acts like it.
1439
1440 Returns:
1441 A modified instance of http that was passed in.
1442
1443 Example:
1444
1445 h = httplib2.Http()
1446 h = tunnel_patch(h, "my-app-name/6.0")
1447
1448 Useful if you are running on a platform that doesn't support PATCH.
1449 Apply this last if you are using OAuth 1.0, as changing the method
1450 will result in a different signature.
1451 """
1452 request_orig = http.request
1453
1454 # The closure that will replace 'httplib2.Http.request'.
1455 def new_request(uri, method='GET', body=None, headers=None,
1456 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1457 connection_type=None):
1458 """Modify the request headers to add the user-agent."""
1459 if headers is None:
1460 headers = {}
1461 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001462 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001463 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001464 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001465 headers['x-http-method-override'] = "PATCH"
1466 method = 'POST'
1467 resp, content = request_orig(uri, method, body, headers,
1468 redirections, connection_type)
1469 return resp, content
1470
1471 http.request = new_request
1472 return http