blob: 96d7a147ad66ad79d8f194261f437c004cc02118 [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
51
Joe Gregoriod0bd3882011-11-22 09:49:47 -050052class MediaUploadProgress(object):
53 """Status of a resumable upload."""
54
55 def __init__(self, resumable_progress, total_size):
56 """Constructor.
57
58 Args:
59 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040060 total_size: int, total bytes in complete upload, or None if the total
61 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050062 """
63 self.resumable_progress = resumable_progress
64 self.total_size = total_size
65
66 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040067 """Percent of upload completed, as a float.
68
69 Returns:
70 the percentage complete as a float, returning 0.0 if the total size of
71 the upload is unknown.
72 """
73 if self.total_size is not None:
74 return float(self.resumable_progress) / float(self.total_size)
75 else:
76 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050077
78
Joe Gregorio708388c2012-06-15 13:43:04 -040079class MediaDownloadProgress(object):
80 """Status of a resumable download."""
81
82 def __init__(self, resumable_progress, total_size):
83 """Constructor.
84
85 Args:
86 resumable_progress: int, bytes received so far.
87 total_size: int, total bytes in complete download.
88 """
89 self.resumable_progress = resumable_progress
90 self.total_size = total_size
91
92 def progress(self):
93 """Percent of download completed, as a float.
94
95 Returns:
96 the percentage complete as a float, returning 0.0 if the total size of
97 the download is unknown.
98 """
99 if self.total_size is not None:
100 return float(self.resumable_progress) / float(self.total_size)
101 else:
102 return 0.0
103
104
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500105class MediaUpload(object):
106 """Describes a media object to upload.
107
108 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400109
110 Note that subclasses of MediaUpload may allow you to control the chunksize
111 when upload a media object. It is important to keep the size of the chunk as
112 large as possible to keep the upload efficient. Other factors may influence
113 the size of the chunk you use, particularly if you are working in an
114 environment where individual HTTP requests may have a hardcoded time limit,
115 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500116 """
117
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500118 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400119 """Chunk size for resumable uploads.
120
121 Returns:
122 Chunk size in bytes.
123 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500124 raise NotImplementedError()
125
126 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400127 """Mime type of the body.
128
129 Returns:
130 Mime type.
131 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500132 return 'application/octet-stream'
133
Joe Gregorio910b9b12012-06-12 09:36:30 -0400134 def size(self):
135 """Size of upload.
136
137 Returns:
138 Size of the body, or None of the size is unknown.
139 """
140 return None
141
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500142 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400143 """Whether this upload is resumable.
144
145 Returns:
146 True if resumable upload or False.
147 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500148 return False
149
Joe Gregorio910b9b12012-06-12 09:36:30 -0400150 def getbytes(self, begin, end):
151 """Get bytes from the media.
152
153 Args:
154 begin: int, offset from beginning of file.
155 length: int, number of bytes to read, starting at begin.
156
157 Returns:
158 A string of bytes read. May be shorter than length if EOF was reached
159 first.
160 """
161 raise NotImplementedError()
162
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500163 def _to_json(self, strip=None):
164 """Utility function for creating a JSON representation of a MediaUpload.
165
166 Args:
167 strip: array, An array of names of members to not include in the JSON.
168
169 Returns:
170 string, a JSON representation of this instance, suitable to pass to
171 from_json().
172 """
173 t = type(self)
174 d = copy.copy(self.__dict__)
175 if strip is not None:
176 for member in strip:
177 del d[member]
178 d['_class'] = t.__name__
179 d['_module'] = t.__module__
180 return simplejson.dumps(d)
181
182 def to_json(self):
183 """Create a JSON representation of an instance of MediaUpload.
184
185 Returns:
186 string, a JSON representation of this instance, suitable to pass to
187 from_json().
188 """
189 return self._to_json()
190
191 @classmethod
192 def new_from_json(cls, s):
193 """Utility class method to instantiate a MediaUpload subclass from a JSON
194 representation produced by to_json().
195
196 Args:
197 s: string, JSON from to_json().
198
199 Returns:
200 An instance of the subclass of MediaUpload that was serialized with
201 to_json().
202 """
203 data = simplejson.loads(s)
204 # Find and call the right classmethod from_json() to restore the object.
205 module = data['_module']
206 m = __import__(module, fromlist=module.split('.')[:-1])
207 kls = getattr(m, data['_class'])
208 from_json = getattr(kls, 'from_json')
209 return from_json(s)
210
Joe Gregorio66f57522011-11-30 11:00:00 -0500211
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500212class MediaFileUpload(MediaUpload):
213 """A MediaUpload for a file.
214
215 Construct a MediaFileUpload and pass as the media_body parameter of the
216 method. For example, if we had a service that allowed uploading images:
217
218
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400219 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400220 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400221 farm.animals()..insert(
222 id='cow',
223 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500224 media_body=media).execute()
225 """
226
Joe Gregorio910b9b12012-06-12 09:36:30 -0400227 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500228 """Constructor.
229
230 Args:
231 filename: string, Name of the file.
232 mimetype: string, Mime-type of the file. If None then a mime-type will be
233 guessed from the file extension.
234 chunksize: int, File will be uploaded in chunks of this many bytes. Only
235 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500236 resumable: bool, True if this is a resumable upload. False means upload
237 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500238 """
239 self._filename = filename
240 self._size = os.path.getsize(filename)
241 self._fd = None
242 if mimetype is None:
243 (mimetype, encoding) = mimetypes.guess_type(filename)
244 self._mimetype = mimetype
245 self._chunksize = chunksize
246 self._resumable = resumable
247
Joe Gregorio910b9b12012-06-12 09:36:30 -0400248 def chunksize(self):
249 """Chunk size for resumable uploads.
250
251 Returns:
252 Chunk size in bytes.
253 """
254 return self._chunksize
255
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500256 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400257 """Mime type of the body.
258
259 Returns:
260 Mime type.
261 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500262 return self._mimetype
263
264 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400265 """Size of upload.
266
267 Returns:
268 Size of the body, or None of the size is unknown.
269 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500270 return self._size
271
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500272 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400273 """Whether this upload is resumable.
274
275 Returns:
276 True if resumable upload or False.
277 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500278 return self._resumable
279
280 def getbytes(self, begin, length):
281 """Get bytes from the media.
282
283 Args:
284 begin: int, offset from beginning of file.
285 length: int, number of bytes to read, starting at begin.
286
287 Returns:
288 A string of bytes read. May be shorted than length if EOF was reached
289 first.
290 """
291 if self._fd is None:
292 self._fd = open(self._filename, 'rb')
293 self._fd.seek(begin)
294 return self._fd.read(length)
295
296 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400297 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500298
299 Returns:
300 string, a JSON representation of this instance, suitable to pass to
301 from_json().
302 """
303 return self._to_json(['_fd'])
304
305 @staticmethod
306 def from_json(s):
307 d = simplejson.loads(s)
308 return MediaFileUpload(
309 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
310
311
Joe Gregorio910b9b12012-06-12 09:36:30 -0400312class MediaIoBaseUpload(MediaUpload):
313 """A MediaUpload for a io.Base objects.
314
315 Note that the Python file object is compatible with io.Base and can be used
316 with this class also.
317
Joe Gregorio910b9b12012-06-12 09:36:30 -0400318 fh = io.BytesIO('...Some data to upload...')
319 media = MediaIoBaseUpload(fh, mimetype='image/png',
320 chunksize=1024*1024, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400321 farm.animals().insert(
322 id='cow',
323 name='cow.png',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400324 media_body=media).execute()
325 """
326
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400327 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400328 resumable=False):
329 """Constructor.
330
331 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400332 fd: io.Base or file object, The source of the bytes to upload. MUST be
Joe Gregorio44454e42012-06-15 08:38:53 -0400333 opened in blocking mode, do not use streams opened in non-blocking mode.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400334 mimetype: string, Mime-type of the file. If None then a mime-type will be
335 guessed from the file extension.
336 chunksize: int, File will be uploaded in chunks of this many bytes. Only
337 used if resumable=True.
338 resumable: bool, True if this is a resumable upload. False means upload
339 in a single request.
340 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400341 self._fd = fd
Joe Gregorio910b9b12012-06-12 09:36:30 -0400342 self._mimetype = mimetype
343 self._chunksize = chunksize
344 self._resumable = resumable
345 self._size = None
346 try:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400347 if hasattr(self._fd, 'fileno'):
348 fileno = self._fd.fileno()
Joe Gregorio44454e42012-06-15 08:38:53 -0400349
350 # Pipes and such show up as 0 length files.
351 size = os.fstat(fileno).st_size
352 if size:
353 self._size = os.fstat(fileno).st_size
Joe Gregorio910b9b12012-06-12 09:36:30 -0400354 except IOError:
355 pass
356
357 def chunksize(self):
358 """Chunk size for resumable uploads.
359
360 Returns:
361 Chunk size in bytes.
362 """
363 return self._chunksize
364
365 def mimetype(self):
366 """Mime type of the body.
367
368 Returns:
369 Mime type.
370 """
371 return self._mimetype
372
373 def size(self):
374 """Size of upload.
375
376 Returns:
377 Size of the body, or None of the size is unknown.
378 """
379 return self._size
380
381 def resumable(self):
382 """Whether this upload is resumable.
383
384 Returns:
385 True if resumable upload or False.
386 """
387 return self._resumable
388
389 def getbytes(self, begin, length):
390 """Get bytes from the media.
391
392 Args:
393 begin: int, offset from beginning of file.
394 length: int, number of bytes to read, starting at begin.
395
396 Returns:
397 A string of bytes read. May be shorted than length if EOF was reached
398 first.
399 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400400 self._fd.seek(begin)
401 return self._fd.read(length)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400402
403 def to_json(self):
404 """This upload type is not serializable."""
405 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
406
407
Ali Afshar6f11ea12012-02-07 10:32:14 -0500408class MediaInMemoryUpload(MediaUpload):
409 """MediaUpload for a chunk of bytes.
410
411 Construct a MediaFileUpload and pass as the media_body parameter of the
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400412 method.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500413 """
414
415 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400416 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500417 """Create a new MediaBytesUpload.
418
419 Args:
420 body: string, Bytes of body content.
421 mimetype: string, Mime-type of the file or default of
422 'application/octet-stream'.
423 chunksize: int, File will be uploaded in chunks of this many bytes. Only
424 used if resumable=True.
425 resumable: bool, True if this is a resumable upload. False means upload
426 in a single request.
427 """
428 self._body = body
429 self._mimetype = mimetype
430 self._resumable = resumable
431 self._chunksize = chunksize
432
433 def chunksize(self):
434 """Chunk size for resumable uploads.
435
436 Returns:
437 Chunk size in bytes.
438 """
439 return self._chunksize
440
441 def mimetype(self):
442 """Mime type of the body.
443
444 Returns:
445 Mime type.
446 """
447 return self._mimetype
448
449 def size(self):
450 """Size of upload.
451
452 Returns:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400453 Size of the body, or None of the size is unknown.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500454 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400455 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500456
457 def resumable(self):
458 """Whether this upload is resumable.
459
460 Returns:
461 True if resumable upload or False.
462 """
463 return self._resumable
464
465 def getbytes(self, begin, length):
466 """Get bytes from the media.
467
468 Args:
469 begin: int, offset from beginning of file.
470 length: int, number of bytes to read, starting at begin.
471
472 Returns:
473 A string of bytes read. May be shorter than length if EOF was reached
474 first.
475 """
476 return self._body[begin:begin + length]
477
478 def to_json(self):
479 """Create a JSON representation of a MediaInMemoryUpload.
480
481 Returns:
482 string, a JSON representation of this instance, suitable to pass to
483 from_json().
484 """
485 t = type(self)
486 d = copy.copy(self.__dict__)
487 del d['_body']
488 d['_class'] = t.__name__
489 d['_module'] = t.__module__
490 d['_b64body'] = base64.b64encode(self._body)
491 return simplejson.dumps(d)
492
493 @staticmethod
494 def from_json(s):
495 d = simplejson.loads(s)
496 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
497 d['_mimetype'], d['_chunksize'],
498 d['_resumable'])
499
500
Joe Gregorio708388c2012-06-15 13:43:04 -0400501class MediaIoBaseDownload(object):
502 """"Download media resources.
503
504 Note that the Python file object is compatible with io.Base and can be used
505 with this class also.
506
507
508 Example:
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400509 request = farms.animals().get_media(id='cow')
510 fh = io.FileIO('cow.png', mode='wb')
Joe Gregorio708388c2012-06-15 13:43:04 -0400511 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
512
513 done = False
514 while done is False:
515 status, done = downloader.next_chunk()
516 if status:
517 print "Download %d%%." % int(status.progress() * 100)
518 print "Download Complete!"
519 """
520
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400521 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
Joe Gregorio708388c2012-06-15 13:43:04 -0400522 """Constructor.
523
524 Args:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400525 fd: io.Base or file object, The stream in which to write the downloaded
Joe Gregorio708388c2012-06-15 13:43:04 -0400526 bytes.
527 request: apiclient.http.HttpRequest, the media request to perform in
528 chunks.
529 chunksize: int, File will be downloaded in chunks of this many bytes.
530 """
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400531 self._fd = fd
532 self._request = request
533 self._uri = request.uri
534 self._chunksize = chunksize
535 self._progress = 0
536 self._total_size = None
537 self._done = False
Joe Gregorio708388c2012-06-15 13:43:04 -0400538
539 def next_chunk(self):
540 """Get the next chunk of the download.
541
542 Returns:
543 (status, done): (MediaDownloadStatus, boolean)
544 The value of 'done' will be True when the media has been fully
545 downloaded.
546
547 Raises:
548 apiclient.errors.HttpError if the response was not a 2xx.
549 httplib2.Error if a transport error has occured.
550 """
551 headers = {
552 'range': 'bytes=%d-%d' % (
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400553 self._progress, self._progress + self._chunksize)
Joe Gregorio708388c2012-06-15 13:43:04 -0400554 }
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400555 http = self._request.http
Joe Gregorio708388c2012-06-15 13:43:04 -0400556 http.follow_redirects = False
557
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400558 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400559 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400560 self._uri = resp['location']
561 resp, content = http.request(self._uri, headers=headers)
Joe Gregorio708388c2012-06-15 13:43:04 -0400562 if resp.status in [200, 206]:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400563 self._progress += len(content)
564 self._fd.write(content)
Joe Gregorio708388c2012-06-15 13:43:04 -0400565
566 if 'content-range' in resp:
567 content_range = resp['content-range']
568 length = content_range.rsplit('/', 1)[1]
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400569 self._total_size = int(length)
Joe Gregorio708388c2012-06-15 13:43:04 -0400570
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400571 if self._progress == self._total_size:
572 self._done = True
573 return MediaDownloadProgress(self._progress, self._total_size), self._done
Joe Gregorio708388c2012-06-15 13:43:04 -0400574 else:
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400575 raise HttpError(resp, content, self._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400576
577
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400578class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500579 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400580
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500581 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500582 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500583 body=None,
584 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500585 methodId=None,
586 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500587 """Constructor for an HttpRequest.
588
Joe Gregorioaf276d22010-12-09 14:26:58 -0500589 Args:
590 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500591 postproc: callable, called on the HTTP response and content to transform
592 it into a data object before returning, or raising an exception
593 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500594 uri: string, the absolute URI to send the request to
595 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500596 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500597 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500598 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500599 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500600 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400601 self.uri = uri
602 self.method = method
603 self.body = body
604 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500605 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400606 self.http = http
607 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500608 self.resumable = resumable
Joe Gregorio910b9b12012-06-12 09:36:30 -0400609 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500610
Joe Gregorio66f57522011-11-30 11:00:00 -0500611 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500612 major, minor, params = mimeparse.parse_mime_type(
613 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500614
Joe Gregorio945be3e2012-01-27 17:01:06 -0500615 # The size of the non-media part of the request.
616 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500617
618 # The resumable URI to send chunks to.
619 self.resumable_uri = None
620
621 # The bytes that have been uploaded.
622 self.resumable_progress = 0
623
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400624 def execute(self, http=None):
625 """Execute the request.
626
Joe Gregorioaf276d22010-12-09 14:26:58 -0500627 Args:
628 http: httplib2.Http, an http object to be used in place of the
629 one the HttpRequest request object was constructed with.
630
631 Returns:
632 A deserialized object model of the response body as determined
633 by the postproc.
634
635 Raises:
636 apiclient.errors.HttpError if the response was not a 2xx.
637 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400638 """
639 if http is None:
640 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500641 if self.resumable:
642 body = None
643 while body is None:
644 _, body = self.next_chunk(http)
645 return body
646 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500647 if 'content-length' not in self.headers:
648 self.headers['content-length'] = str(self.body_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500649 resp, content = http.request(self.uri, self.method,
650 body=self.body,
651 headers=self.headers)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500652 if resp.status >= 300:
653 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400654 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500655
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500656 def next_chunk(self, http=None):
657 """Execute the next step of a resumable upload.
658
Joe Gregorio66f57522011-11-30 11:00:00 -0500659 Can only be used if the method being executed supports media uploads and
660 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500661
662 Example:
663
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400664 media = MediaFileUpload('cow.png', mimetype='image/png',
Joe Gregorio66f57522011-11-30 11:00:00 -0500665 chunksize=1000, resumable=True)
Joe Gregorio7ceb26f2012-06-15 13:57:26 -0400666 request = farm.animals().insert(
667 id='cow',
668 name='cow.png',
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500669 media_body=media)
670
671 response = None
672 while response is None:
673 status, response = request.next_chunk()
674 if status:
675 print "Upload %d%% complete." % int(status.progress() * 100)
676
677
678 Returns:
679 (status, body): (ResumableMediaStatus, object)
680 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400681
682 Raises:
683 apiclient.errors.HttpError if the response was not a 2xx.
684 httplib2.Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500685 """
686 if http is None:
687 http = self.http
688
Joe Gregorio910b9b12012-06-12 09:36:30 -0400689 if self.resumable.size() is None:
690 size = '*'
691 else:
692 size = str(self.resumable.size())
693
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500694 if self.resumable_uri is None:
695 start_headers = copy.copy(self.headers)
696 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400697 if size != '*':
698 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500699 start_headers['content-length'] = str(self.body_size)
700
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500701 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500702 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500703 headers=start_headers)
704 if resp.status == 200 and 'location' in resp:
705 self.resumable_uri = resp['location']
706 else:
707 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregorio910b9b12012-06-12 09:36:30 -0400708 elif self._in_error_state:
709 # If we are in an error state then query the server for current state of
710 # the upload by sending an empty PUT and reading the 'range' header in
711 # the response.
712 headers = {
713 'Content-Range': 'bytes */%s' % size,
714 'content-length': '0'
715 }
716 resp, content = http.request(self.resumable_uri, 'PUT',
717 headers=headers)
718 status, body = self._process_response(resp, content)
719 if body:
720 # The upload was complete.
721 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500722
Joe Gregorio910b9b12012-06-12 09:36:30 -0400723 data = self.resumable.getbytes(
724 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400725
726 # A short read implies that we are at EOF, so finish the upload.
727 if len(data) < self.resumable.chunksize():
728 size = str(self.resumable_progress + len(data))
729
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500730 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400731 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500732 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400733 size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500734 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400735 try:
736 resp, content = http.request(self.resumable_uri, 'PUT',
737 body=data,
738 headers=headers)
739 except:
740 self._in_error_state = True
741 raise
742
743 return self._process_response(resp, content)
744
745 def _process_response(self, resp, content):
746 """Process the response from a single chunk upload.
747
748 Args:
749 resp: httplib2.Response, the response object.
750 content: string, the content of the response.
751
752 Returns:
753 (status, body): (ResumableMediaStatus, object)
754 The body will be None until the resumable media is fully uploaded.
755
756 Raises:
757 apiclient.errors.HttpError if the response was not a 2xx or a 308.
758 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500759 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400760 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500761 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500762 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400763 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500764 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500765 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500766 if 'location' in resp:
767 self.resumable_uri = resp['location']
768 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400769 self._in_error_state = True
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500770 raise HttpError(resp, content, self.uri)
771
Joe Gregorio945be3e2012-01-27 17:01:06 -0500772 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
773 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500774
775 def to_json(self):
776 """Returns a JSON representation of the HttpRequest."""
777 d = copy.copy(self.__dict__)
778 if d['resumable'] is not None:
779 d['resumable'] = self.resumable.to_json()
780 del d['http']
781 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400782
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500783 return simplejson.dumps(d)
784
785 @staticmethod
786 def from_json(s, http, postproc):
787 """Returns an HttpRequest populated with info from a JSON object."""
788 d = simplejson.loads(s)
789 if d['resumable'] is not None:
790 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
791 return HttpRequest(
792 http,
793 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500794 uri=d['uri'],
795 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500796 body=d['body'],
797 headers=d['headers'],
798 methodId=d['methodId'],
799 resumable=d['resumable'])
800
Joe Gregorioaf276d22010-12-09 14:26:58 -0500801
Joe Gregorio66f57522011-11-30 11:00:00 -0500802class BatchHttpRequest(object):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400803 """Batches multiple HttpRequest objects into a single HTTP request.
804
805 Example:
806 from apiclient.http import BatchHttpRequest
807
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400808 def list_animals(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400809 \"\"\"Do something with the animals list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400810 if exception is not None:
811 # Do something with the exception.
812 pass
813 else:
814 # Do something with the response.
815 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400816
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400817 def list_farmers(request_id, response, exception):
Joe Gregorioebd0b842012-06-15 14:14:17 -0400818 \"\"\"Do something with the farmers list response.\"\"\"
Joe Gregorioe7a0c472012-07-12 11:46:04 -0400819 if exception is not None:
820 # Do something with the exception.
821 pass
822 else:
823 # Do something with the response.
824 pass
Joe Gregorioebd0b842012-06-15 14:14:17 -0400825
826 service = build('farm', 'v2')
827
828 batch = BatchHttpRequest()
829
830 batch.add(service.animals().list(), list_animals)
831 batch.add(service.farmers().list(), list_farmers)
832 batch.execute(http)
833 """
Joe Gregorio66f57522011-11-30 11:00:00 -0500834
835 def __init__(self, callback=None, batch_uri=None):
836 """Constructor for a BatchHttpRequest.
837
838 Args:
839 callback: callable, A callback to be called for each response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -0400840 form callback(id, response, exception). The first parameter is the
841 request id, and the second is the deserialized response object. The
842 third is an apiclient.errors.HttpError exception object if an HTTP error
843 occurred while processing the request, or None if no error occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -0500844 batch_uri: string, URI to send batch requests to.
845 """
846 if batch_uri is None:
847 batch_uri = 'https://www.googleapis.com/batch'
848 self._batch_uri = batch_uri
849
850 # Global callback to be called for each individual response in the batch.
851 self._callback = callback
852
Joe Gregorio654f4a22012-02-09 14:15:44 -0500853 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500854 self._requests = {}
855
Joe Gregorio654f4a22012-02-09 14:15:44 -0500856 # A map from id to callback.
857 self._callbacks = {}
858
Joe Gregorio66f57522011-11-30 11:00:00 -0500859 # List of request ids, in the order in which they were added.
860 self._order = []
861
862 # The last auto generated id.
863 self._last_auto_id = 0
864
865 # Unique ID on which to base the Content-ID headers.
866 self._base_id = None
867
Joe Gregorioc752e332012-07-11 14:43:52 -0400868 # A map from request id to (httplib2.Response, content) response pairs
Joe Gregorio654f4a22012-02-09 14:15:44 -0500869 self._responses = {}
870
871 # A map of id(Credentials) that have been refreshed.
872 self._refreshed_credentials = {}
873
874 def _refresh_and_apply_credentials(self, request, http):
875 """Refresh the credentials and apply to the request.
876
877 Args:
878 request: HttpRequest, the request.
879 http: httplib2.Http, the global http object for the batch.
880 """
881 # For the credentials to refresh, but only once per refresh_token
882 # If there is no http per the request then refresh the http passed in
883 # via execute()
884 creds = None
885 if request.http is not None and hasattr(request.http.request,
886 'credentials'):
887 creds = request.http.request.credentials
888 elif http is not None and hasattr(http.request, 'credentials'):
889 creds = http.request.credentials
890 if creds is not None:
891 if id(creds) not in self._refreshed_credentials:
892 creds.refresh(http)
893 self._refreshed_credentials[id(creds)] = 1
894
895 # Only apply the credentials if we are using the http object passed in,
896 # otherwise apply() will get called during _serialize_request().
897 if request.http is None or not hasattr(request.http.request,
898 'credentials'):
899 creds.apply(request.headers)
900
Joe Gregorio66f57522011-11-30 11:00:00 -0500901 def _id_to_header(self, id_):
902 """Convert an id to a Content-ID header value.
903
904 Args:
905 id_: string, identifier of individual request.
906
907 Returns:
908 A Content-ID header with the id_ encoded into it. A UUID is prepended to
909 the value because Content-ID headers are supposed to be universally
910 unique.
911 """
912 if self._base_id is None:
913 self._base_id = uuid.uuid4()
914
915 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
916
917 def _header_to_id(self, header):
918 """Convert a Content-ID header value to an id.
919
920 Presumes the Content-ID header conforms to the format that _id_to_header()
921 returns.
922
923 Args:
924 header: string, Content-ID header value.
925
926 Returns:
927 The extracted id value.
928
929 Raises:
930 BatchError if the header is not in the expected format.
931 """
932 if header[0] != '<' or header[-1] != '>':
933 raise BatchError("Invalid value for Content-ID: %s" % header)
934 if '+' not in header:
935 raise BatchError("Invalid value for Content-ID: %s" % header)
936 base, id_ = header[1:-1].rsplit('+', 1)
937
938 return urllib.unquote(id_)
939
940 def _serialize_request(self, request):
941 """Convert an HttpRequest object into a string.
942
943 Args:
944 request: HttpRequest, the request to serialize.
945
946 Returns:
947 The request as a string in application/http format.
948 """
949 # Construct status line
950 parsed = urlparse.urlparse(request.uri)
951 request_line = urlparse.urlunparse(
952 (None, None, parsed.path, parsed.params, parsed.query, None)
953 )
954 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500955 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500956 msg = MIMENonMultipart(major, minor)
957 headers = request.headers.copy()
958
Joe Gregorio654f4a22012-02-09 14:15:44 -0500959 if request.http is not None and hasattr(request.http.request,
960 'credentials'):
961 request.http.request.credentials.apply(headers)
962
Joe Gregorio66f57522011-11-30 11:00:00 -0500963 # MIMENonMultipart adds its own Content-Type header.
964 if 'content-type' in headers:
965 del headers['content-type']
966
967 for key, value in headers.iteritems():
968 msg[key] = value
969 msg['Host'] = parsed.netloc
970 msg.set_unixfrom(None)
971
972 if request.body is not None:
973 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500974 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500975
Joe Gregorio654f4a22012-02-09 14:15:44 -0500976 # Serialize the mime message.
977 fp = StringIO.StringIO()
978 # maxheaderlen=0 means don't line wrap headers.
979 g = Generator(fp, maxheaderlen=0)
980 g.flatten(msg, unixfrom=False)
981 body = fp.getvalue()
982
Joe Gregorio66f57522011-11-30 11:00:00 -0500983 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
984 if request.body is None:
985 body = body[:-2]
986
Joe Gregoriodd813822012-01-25 10:32:47 -0500987 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500988
989 def _deserialize_response(self, payload):
990 """Convert string into httplib2 response and content.
991
992 Args:
993 payload: string, headers and body as a string.
994
995 Returns:
Joe Gregorioc752e332012-07-11 14:43:52 -0400996 A pair (resp, content), such as would be returned from httplib2.request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500997 """
998 # Strip off the status line
999 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001000 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -05001001
1002 # Parse the rest of the response
1003 parser = FeedParser()
1004 parser.feed(payload)
1005 msg = parser.close()
1006 msg['status'] = status
1007
1008 # Create httplib2.Response from the parsed headers.
1009 resp = httplib2.Response(msg)
1010 resp.reason = reason
1011 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1012
1013 content = payload.split('\r\n\r\n', 1)[1]
1014
1015 return resp, content
1016
1017 def _new_id(self):
1018 """Create a new id.
1019
1020 Auto incrementing number that avoids conflicts with ids already used.
1021
1022 Returns:
1023 string, a new unique id.
1024 """
1025 self._last_auto_id += 1
1026 while str(self._last_auto_id) in self._requests:
1027 self._last_auto_id += 1
1028 return str(self._last_auto_id)
1029
1030 def add(self, request, callback=None, request_id=None):
1031 """Add a new request.
1032
1033 Every callback added will be paired with a unique id, the request_id. That
1034 unique id will be passed back to the callback when the response comes back
1035 from the server. The default behavior is to have the library generate it's
1036 own unique id. If the caller passes in a request_id then they must ensure
1037 uniqueness for each request_id, and if they are not an exception is
1038 raised. Callers should either supply all request_ids or nevery supply a
1039 request id, to avoid such an error.
1040
1041 Args:
1042 request: HttpRequest, Request to add to the batch.
1043 callback: callable, A callback to be called for this response, of the
Joe Gregorio4fbde1c2012-07-11 14:47:39 -04001044 form callback(id, response, exception). The first parameter is the
1045 request id, and the second is the deserialized response object. The
1046 third is an apiclient.errors.HttpError exception object if an HTTP error
1047 occurred while processing the request, or None if no errors occurred.
Joe Gregorio66f57522011-11-30 11:00:00 -05001048 request_id: string, A unique id for the request. The id will be passed to
1049 the callback with the response.
1050
1051 Returns:
1052 None
1053
1054 Raises:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001055 BatchError if a media request is added to a batch.
Joe Gregorio66f57522011-11-30 11:00:00 -05001056 KeyError is the request_id is not unique.
1057 """
1058 if request_id is None:
1059 request_id = self._new_id()
1060 if request.resumable is not None:
Joe Gregorioebd0b842012-06-15 14:14:17 -04001061 raise BatchError("Media requests cannot be used in a batch request.")
Joe Gregorio66f57522011-11-30 11:00:00 -05001062 if request_id in self._requests:
1063 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001064 self._requests[request_id] = request
1065 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001066 self._order.append(request_id)
1067
Joe Gregorio654f4a22012-02-09 14:15:44 -05001068 def _execute(self, http, order, requests):
1069 """Serialize batch request, send to server, process response.
1070
1071 Args:
1072 http: httplib2.Http, an http object to be used to make the request with.
1073 order: list, list of request ids in the order they were added to the
1074 batch.
1075 request: list, list of request objects to send.
1076
1077 Raises:
1078 httplib2.Error if a transport error has occured.
1079 apiclient.errors.BatchError if the response is the wrong format.
1080 """
1081 message = MIMEMultipart('mixed')
1082 # Message should not write out it's own headers.
1083 setattr(message, '_write_headers', lambda self: None)
1084
1085 # Add all the individual requests.
1086 for request_id in order:
1087 request = requests[request_id]
1088
1089 msg = MIMENonMultipart('application', 'http')
1090 msg['Content-Transfer-Encoding'] = 'binary'
1091 msg['Content-ID'] = self._id_to_header(request_id)
1092
1093 body = self._serialize_request(request)
1094 msg.set_payload(body)
1095 message.attach(msg)
1096
1097 body = message.as_string()
1098
1099 headers = {}
1100 headers['content-type'] = ('multipart/mixed; '
1101 'boundary="%s"') % message.get_boundary()
1102
1103 resp, content = http.request(self._batch_uri, 'POST', body=body,
1104 headers=headers)
1105
1106 if resp.status >= 300:
1107 raise HttpError(resp, content, self._batch_uri)
1108
1109 # Now break out the individual responses and store each one.
1110 boundary, _ = content.split(None, 1)
1111
1112 # Prepend with a content-type header so FeedParser can handle it.
1113 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1114 for_parser = header + content
1115
1116 parser = FeedParser()
1117 parser.feed(for_parser)
1118 mime_response = parser.close()
1119
1120 if not mime_response.is_multipart():
1121 raise BatchError("Response not in multipart/mixed format.", resp,
1122 content)
1123
1124 for part in mime_response.get_payload():
1125 request_id = self._header_to_id(part['Content-ID'])
Joe Gregorioc752e332012-07-11 14:43:52 -04001126 response, content = self._deserialize_response(part.get_payload())
1127 self._responses[request_id] = (response, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001128
Joe Gregorio66f57522011-11-30 11:00:00 -05001129 def execute(self, http=None):
1130 """Execute all the requests as a single batched HTTP request.
1131
1132 Args:
1133 http: httplib2.Http, an http object to be used in place of the one the
1134 HttpRequest request object was constructed with. If one isn't supplied
1135 then use a http object from the requests in this batch.
1136
1137 Returns:
1138 None
1139
1140 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -05001141 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001142 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001143 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001144
1145 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001146 if http is None:
1147 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001148 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001149 if request is not None:
1150 http = request.http
1151 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001152
Joe Gregorio66f57522011-11-30 11:00:00 -05001153 if http is None:
1154 raise ValueError("Missing a valid http object.")
1155
Joe Gregorio654f4a22012-02-09 14:15:44 -05001156 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001157
Joe Gregorio654f4a22012-02-09 14:15:44 -05001158 # Loop over all the requests and check for 401s. For each 401 request the
1159 # credentials should be refreshed and then sent again in a separate batch.
1160 redo_requests = {}
1161 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001162
Joe Gregorio66f57522011-11-30 11:00:00 -05001163 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001164 resp, content = self._responses[request_id]
1165 if resp['status'] == '401':
Joe Gregorio654f4a22012-02-09 14:15:44 -05001166 redo_order.append(request_id)
1167 request = self._requests[request_id]
1168 self._refresh_and_apply_credentials(request, http)
1169 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001170
Joe Gregorio654f4a22012-02-09 14:15:44 -05001171 if redo_requests:
1172 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001173
Joe Gregorio654f4a22012-02-09 14:15:44 -05001174 # Now process all callbacks that are erroring, and raise an exception for
1175 # ones that return a non-2xx response? Or add extra parameter to callback
1176 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001177
Joe Gregorio654f4a22012-02-09 14:15:44 -05001178 for request_id in self._order:
Joe Gregorioc752e332012-07-11 14:43:52 -04001179 resp, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001180
Joe Gregorio654f4a22012-02-09 14:15:44 -05001181 request = self._requests[request_id]
1182 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001183
Joe Gregorio654f4a22012-02-09 14:15:44 -05001184 response = None
1185 exception = None
1186 try:
Joe Gregorio3fb93672012-07-25 11:31:11 -04001187 if resp.status >= 300:
1188 raise HttpError(resp, content, request.uri)
Joe Gregorioc752e332012-07-11 14:43:52 -04001189 response = request.postproc(resp, content)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001190 except HttpError, e:
1191 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001192
Joe Gregorio654f4a22012-02-09 14:15:44 -05001193 if callback is not None:
1194 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001195 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001196 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001197
1198
Joe Gregorioaf276d22010-12-09 14:26:58 -05001199class HttpRequestMock(object):
1200 """Mock of HttpRequest.
1201
1202 Do not construct directly, instead use RequestMockBuilder.
1203 """
1204
1205 def __init__(self, resp, content, postproc):
1206 """Constructor for HttpRequestMock
1207
1208 Args:
1209 resp: httplib2.Response, the response to emulate coming from the request
1210 content: string, the response body
1211 postproc: callable, the post processing function usually supplied by
1212 the model class. See model.JsonModel.response() as an example.
1213 """
1214 self.resp = resp
1215 self.content = content
1216 self.postproc = postproc
1217 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001218 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001219 if 'reason' in self.resp:
1220 self.resp.reason = self.resp['reason']
1221
1222 def execute(self, http=None):
1223 """Execute the request.
1224
1225 Same behavior as HttpRequest.execute(), but the response is
1226 mocked and not really from an HTTP request/response.
1227 """
1228 return self.postproc(self.resp, self.content)
1229
1230
1231class RequestMockBuilder(object):
1232 """A simple mock of HttpRequest
1233
1234 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001235 tuples of (httplib2.Response, content, opt_expected_body) that should be
1236 returned when that method is called. None may also be passed in for the
1237 httplib2.Response, in which case a 200 OK response will be generated.
1238 If an opt_expected_body (str or dict) is provided, it will be compared to
1239 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001240
1241 Example:
1242 response = '{"data": {"id": "tag:google.c...'
1243 requestBuilder = RequestMockBuilder(
1244 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001245 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001246 }
1247 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001248 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001249
1250 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001251 200 OK with an empty string as the response content or raise an excpetion
1252 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001253 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001254
1255 For more details see the project wiki.
1256 """
1257
Joe Gregorioa388ce32011-09-09 17:19:13 -04001258 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001259 """Constructor for RequestMockBuilder
1260
1261 The constructed object should be a callable object
1262 that can replace the class HttpResponse.
1263
1264 responses - A dictionary that maps methodIds into tuples
1265 of (httplib2.Response, content). The methodId
1266 comes from the 'rpcName' field in the discovery
1267 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001268 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1269 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001270 """
1271 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001272 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001273
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001274 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001275 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001276 """Implements the callable interface that discovery.build() expects
1277 of requestBuilder, which is to build an object compatible with
1278 HttpRequest.execute(). See that method for the description of the
1279 parameters and the expected response.
1280 """
1281 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001282 response = self.responses[methodId]
1283 resp, content = response[:2]
1284 if len(response) > 2:
1285 # Test the body against the supplied expected_body.
1286 expected_body = response[2]
1287 if bool(expected_body) != bool(body):
1288 # Not expecting a body and provided one
1289 # or expecting a body and not provided one.
1290 raise UnexpectedBodyError(expected_body, body)
1291 if isinstance(expected_body, str):
1292 expected_body = simplejson.loads(expected_body)
1293 body = simplejson.loads(body)
1294 if body != expected_body:
1295 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001296 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001297 elif self.check_unexpected:
1298 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001299 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001300 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001301 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001302
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001303
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001304class HttpMock(object):
1305 """Mock of httplib2.Http"""
1306
Joe Gregorioec343652011-02-16 16:52:51 -05001307 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001308 """
1309 Args:
1310 filename: string, absolute filename to read response from
1311 headers: dict, header to return with response
1312 """
Joe Gregorioec343652011-02-16 16:52:51 -05001313 if headers is None:
1314 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001315 f = file(filename, 'r')
1316 self.data = f.read()
1317 f.close()
1318 self.headers = headers
1319
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001320 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001321 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001322 body=None,
1323 headers=None,
1324 redirections=1,
1325 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001326 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001327
1328
1329class HttpMockSequence(object):
1330 """Mock of httplib2.Http
1331
1332 Mocks a sequence of calls to request returning different responses for each
1333 call. Create an instance initialized with the desired response headers
1334 and content and then use as if an httplib2.Http instance.
1335
1336 http = HttpMockSequence([
1337 ({'status': '401'}, ''),
1338 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1339 ({'status': '200'}, 'echo_request_headers'),
1340 ])
1341 resp, content = http.request("http://examples.com")
1342
1343 There are special values you can pass in for content to trigger
1344 behavours that are helpful in testing.
1345
1346 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001347 'echo_request_headers_as_json' means return the request headers in
1348 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001349 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001350 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001351 """
1352
1353 def __init__(self, iterable):
1354 """
1355 Args:
1356 iterable: iterable, a sequence of pairs of (headers, body)
1357 """
1358 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001359 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001360
1361 def request(self, uri,
1362 method='GET',
1363 body=None,
1364 headers=None,
1365 redirections=1,
1366 connection_type=None):
1367 resp, content = self._iterable.pop(0)
1368 if content == 'echo_request_headers':
1369 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001370 elif content == 'echo_request_headers_as_json':
1371 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001372 elif content == 'echo_request_body':
1373 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001374 elif content == 'echo_request_uri':
1375 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001376 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001377
1378
1379def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001380 """Set the user-agent on every request.
1381
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001382 Args:
1383 http - An instance of httplib2.Http
1384 or something that acts like it.
1385 user_agent: string, the value for the user-agent header.
1386
1387 Returns:
1388 A modified instance of http that was passed in.
1389
1390 Example:
1391
1392 h = httplib2.Http()
1393 h = set_user_agent(h, "my-app-name/6.0")
1394
1395 Most of the time the user-agent will be set doing auth, this is for the rare
1396 cases where you are accessing an unauthenticated endpoint.
1397 """
1398 request_orig = http.request
1399
1400 # The closure that will replace 'httplib2.Http.request'.
1401 def new_request(uri, method='GET', body=None, headers=None,
1402 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1403 connection_type=None):
1404 """Modify the request headers to add the user-agent."""
1405 if headers is None:
1406 headers = {}
1407 if 'user-agent' in headers:
1408 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1409 else:
1410 headers['user-agent'] = user_agent
1411 resp, content = request_orig(uri, method, body, headers,
1412 redirections, connection_type)
1413 return resp, content
1414
1415 http.request = new_request
1416 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001417
1418
1419def tunnel_patch(http):
1420 """Tunnel PATCH requests over POST.
1421 Args:
1422 http - An instance of httplib2.Http
1423 or something that acts like it.
1424
1425 Returns:
1426 A modified instance of http that was passed in.
1427
1428 Example:
1429
1430 h = httplib2.Http()
1431 h = tunnel_patch(h, "my-app-name/6.0")
1432
1433 Useful if you are running on a platform that doesn't support PATCH.
1434 Apply this last if you are using OAuth 1.0, as changing the method
1435 will result in a different signature.
1436 """
1437 request_orig = http.request
1438
1439 # The closure that will replace 'httplib2.Http.request'.
1440 def new_request(uri, method='GET', body=None, headers=None,
1441 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1442 connection_type=None):
1443 """Modify the request headers to add the user-agent."""
1444 if headers is None:
1445 headers = {}
1446 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001447 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001448 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001449 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001450 headers['x-http-method-override'] = "PATCH"
1451 method = 'POST'
1452 resp, content = request_orig(uri, method, body, headers,
1453 redirections, connection_type)
1454 return resp, content
1455
1456 http.request = new_request
1457 return http