blob: 7992ea3804fdf8060933a4e1241aa19c20960678 [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -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.
14
15"""Classes to encapsulate a single HTTP request.
16
17The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actuall HTTP request.
20"""
INADA Naoki0bceb332014-08-20 15:27:52 +090021from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090022import six
eesheeshc6425a02016-02-12 15:07:06 +000023from six.moves import http_client
INADA Naokie4ea1a92015-03-04 03:45:42 +090024from six.moves import range
John Asmuth864311d2014-04-24 15:46:08 -040025
26__author__ = 'jcgregorio@google.com (Joe Gregorio)'
27
Pat Ferateed9affd2015-03-03 16:03:15 -080028from six import BytesIO, StringIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080029from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
Pat Ferateed9affd2015-03-03 16:03:15 -080030
John Asmuth864311d2014-04-24 15:46:08 -040031import base64
32import copy
33import gzip
34import httplib2
Craig Citro6ae34d72014-08-18 23:10:09 -070035import json
John Asmuth864311d2014-04-24 15:46:08 -040036import logging
John Asmuth864311d2014-04-24 15:46:08 -040037import mimetypes
38import os
39import random
eesheeshc6425a02016-02-12 15:07:06 +000040import socket
John Asmuth864311d2014-04-24 15:46:08 -040041import sys
42import time
John Asmuth864311d2014-04-24 15:46:08 -040043import uuid
44
Tay Ray Chuan3146c922016-04-20 16:38:19 +000045# TODO(issue 221): Remove this conditional import jibbajabba.
46try:
47 import ssl
48except ImportError:
49 _ssl_SSLError = object()
50else:
51 _ssl_SSLError = ssl.SSLError
52
John Asmuth864311d2014-04-24 15:46:08 -040053from email.generator import Generator
54from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
56from email.parser import FeedParser
Pat Ferateb240c172015-03-03 16:23:51 -080057
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070058# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
59# in '_helpers'.
60try:
61 from oauth2client import util
62except ImportError:
63 from oauth2client import _helpers as util
64
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070065from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080066from googleapiclient import mimeparse
67from googleapiclient.errors import BatchError
68from googleapiclient.errors import HttpError
69from googleapiclient.errors import InvalidChunkSizeError
70from googleapiclient.errors import ResumableUploadError
71from googleapiclient.errors import UnexpectedBodyError
72from googleapiclient.errors import UnexpectedMethodError
73from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040074
75
Emmett Butler09699152016-02-08 14:26:00 -080076LOGGER = logging.getLogger(__name__)
77
John Asmuth864311d2014-04-24 15:46:08 -040078DEFAULT_CHUNK_SIZE = 512*1024
79
80MAX_URI_LENGTH = 2048
81
eesheeshc6425a02016-02-12 15:07:06 +000082_TOO_MANY_REQUESTS = 429
83
Igor Maravić22435292017-01-19 22:28:22 +010084DEFAULT_HTTP_TIMEOUT_SEC = 60
85
eesheeshc6425a02016-02-12 15:07:06 +000086
87def _should_retry_response(resp_status, content):
88 """Determines whether a response should be retried.
89
90 Args:
91 resp_status: The response status received.
92 content: The response content body.
93
94 Returns:
95 True if the response should be retried, otherwise False.
96 """
97 # Retry on 5xx errors.
98 if resp_status >= 500:
99 return True
100
101 # Retry on 429 errors.
102 if resp_status == _TOO_MANY_REQUESTS:
103 return True
104
105 # For 403 errors, we have to check for the `reason` in the response to
106 # determine if we should retry.
107 if resp_status == six.moves.http_client.FORBIDDEN:
108 # If there's no details about the 403 type, don't retry.
109 if not content:
110 return False
111
112 # Content is in JSON format.
113 try:
114 data = json.loads(content.decode('utf-8'))
115 reason = data['error']['errors'][0]['reason']
116 except (UnicodeDecodeError, ValueError, KeyError):
117 LOGGER.warning('Invalid JSON content from response: %s', content)
118 return False
119
120 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
121
122 # Only retry on rate limit related failures.
123 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
124 return True
125
126 # Everything else is a success or non-retriable so break.
127 return False
128
John Asmuth864311d2014-04-24 15:46:08 -0400129
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100130def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
131 **kwargs):
132 """Retries an HTTP request multiple times while handling errors.
133
134 If after all retries the request still fails, last error is either returned as
135 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
136
137 Args:
138 http: Http object to be used to execute request.
139 num_retries: Maximum number of retries.
140 req_type: Type of the request (used for logging retries).
141 sleep, rand: Functions to sleep for random time between retries.
142 uri: URI to be requested.
143 method: HTTP method to be used.
144 args, kwargs: Additional arguments passed to http.request.
145
146 Returns:
147 resp, content - Response from the http request (may be HTTP 5xx).
148 """
149 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000150 content = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100151 for retry_num in range(num_retries + 1):
152 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000153 # Sleep before retrying.
154 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800155 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000156 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
157 sleep_time, retry_num, num_retries, req_type, method, uri,
158 resp.status if resp else exception)
159 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100160
161 try:
eesheeshc6425a02016-02-12 15:07:06 +0000162 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100163 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000164 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000165 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000166 exception = ssl_error
167 except socket.error as socket_error:
168 # errno's contents differ by platform, so we have to match by name.
169 if socket.errno.errorcode.get(socket_error.errno) not in (
Thomas Bonfort88ab76b2016-04-19 08:48:53 +0200170 'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED', ):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100171 raise
eesheeshc6425a02016-02-12 15:07:06 +0000172 exception = socket_error
173
174 if exception:
175 if retry_num == num_retries:
176 raise exception
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100177 else:
178 continue
eesheeshc6425a02016-02-12 15:07:06 +0000179
180 if not _should_retry_response(resp.status, content):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100181 break
182
183 return resp, content
184
185
John Asmuth864311d2014-04-24 15:46:08 -0400186class MediaUploadProgress(object):
187 """Status of a resumable upload."""
188
189 def __init__(self, resumable_progress, total_size):
190 """Constructor.
191
192 Args:
193 resumable_progress: int, bytes sent so far.
194 total_size: int, total bytes in complete upload, or None if the total
195 upload size isn't known ahead of time.
196 """
197 self.resumable_progress = resumable_progress
198 self.total_size = total_size
199
200 def progress(self):
201 """Percent of upload completed, as a float.
202
203 Returns:
204 the percentage complete as a float, returning 0.0 if the total size of
205 the upload is unknown.
206 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300207 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400208 return float(self.resumable_progress) / float(self.total_size)
209 else:
210 return 0.0
211
212
213class MediaDownloadProgress(object):
214 """Status of a resumable download."""
215
216 def __init__(self, resumable_progress, total_size):
217 """Constructor.
218
219 Args:
220 resumable_progress: int, bytes received so far.
221 total_size: int, total bytes in complete download.
222 """
223 self.resumable_progress = resumable_progress
224 self.total_size = total_size
225
226 def progress(self):
227 """Percent of download completed, as a float.
228
229 Returns:
230 the percentage complete as a float, returning 0.0 if the total size of
231 the download is unknown.
232 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300233 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400234 return float(self.resumable_progress) / float(self.total_size)
235 else:
236 return 0.0
237
238
239class MediaUpload(object):
240 """Describes a media object to upload.
241
242 Base class that defines the interface of MediaUpload subclasses.
243
244 Note that subclasses of MediaUpload may allow you to control the chunksize
245 when uploading a media object. It is important to keep the size of the chunk
246 as large as possible to keep the upload efficient. Other factors may influence
247 the size of the chunk you use, particularly if you are working in an
248 environment where individual HTTP requests may have a hardcoded time limit,
249 such as under certain classes of requests under Google App Engine.
250
251 Streams are io.Base compatible objects that support seek(). Some MediaUpload
252 subclasses support using streams directly to upload data. Support for
253 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
254 platform that stream will be used for uploading the media object. The support
255 for streaming is indicated by has_stream() returning True. The stream() method
256 should return an io.Base object that supports seek(). On platforms where the
257 underlying httplib module supports streaming, for example Python 2.6 and
258 later, the stream will be passed into the http library which will result in
259 less memory being used and possibly faster uploads.
260
261 If you need to upload media that can't be uploaded using any of the existing
262 MediaUpload sub-class then you can sub-class MediaUpload for your particular
263 needs.
264 """
265
266 def chunksize(self):
267 """Chunk size for resumable uploads.
268
269 Returns:
270 Chunk size in bytes.
271 """
272 raise NotImplementedError()
273
274 def mimetype(self):
275 """Mime type of the body.
276
277 Returns:
278 Mime type.
279 """
280 return 'application/octet-stream'
281
282 def size(self):
283 """Size of upload.
284
285 Returns:
286 Size of the body, or None of the size is unknown.
287 """
288 return None
289
290 def resumable(self):
291 """Whether this upload is resumable.
292
293 Returns:
294 True if resumable upload or False.
295 """
296 return False
297
298 def getbytes(self, begin, end):
299 """Get bytes from the media.
300
301 Args:
302 begin: int, offset from beginning of file.
303 length: int, number of bytes to read, starting at begin.
304
305 Returns:
306 A string of bytes read. May be shorter than length if EOF was reached
307 first.
308 """
309 raise NotImplementedError()
310
311 def has_stream(self):
312 """Does the underlying upload support a streaming interface.
313
314 Streaming means it is an io.IOBase subclass that supports seek, i.e.
315 seekable() returns True.
316
317 Returns:
318 True if the call to stream() will return an instance of a seekable io.Base
319 subclass.
320 """
321 return False
322
323 def stream(self):
324 """A stream interface to the data being uploaded.
325
326 Returns:
327 The returned value is an io.IOBase subclass that supports seek, i.e.
328 seekable() returns True.
329 """
330 raise NotImplementedError()
331
332 @util.positional(1)
333 def _to_json(self, strip=None):
334 """Utility function for creating a JSON representation of a MediaUpload.
335
336 Args:
337 strip: array, An array of names of members to not include in the JSON.
338
339 Returns:
340 string, a JSON representation of this instance, suitable to pass to
341 from_json().
342 """
343 t = type(self)
344 d = copy.copy(self.__dict__)
345 if strip is not None:
346 for member in strip:
347 del d[member]
348 d['_class'] = t.__name__
349 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700350 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400351
352 def to_json(self):
353 """Create a JSON representation of an instance of MediaUpload.
354
355 Returns:
356 string, a JSON representation of this instance, suitable to pass to
357 from_json().
358 """
359 return self._to_json()
360
361 @classmethod
362 def new_from_json(cls, s):
363 """Utility class method to instantiate a MediaUpload subclass from a JSON
364 representation produced by to_json().
365
366 Args:
367 s: string, JSON from to_json().
368
369 Returns:
370 An instance of the subclass of MediaUpload that was serialized with
371 to_json().
372 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700373 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400374 # Find and call the right classmethod from_json() to restore the object.
375 module = data['_module']
376 m = __import__(module, fromlist=module.split('.')[:-1])
377 kls = getattr(m, data['_class'])
378 from_json = getattr(kls, 'from_json')
379 return from_json(s)
380
381
382class MediaIoBaseUpload(MediaUpload):
383 """A MediaUpload for a io.Base objects.
384
385 Note that the Python file object is compatible with io.Base and can be used
386 with this class also.
387
Pat Ferateed9affd2015-03-03 16:03:15 -0800388 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400389 media = MediaIoBaseUpload(fh, mimetype='image/png',
390 chunksize=1024*1024, resumable=True)
391 farm.animals().insert(
392 id='cow',
393 name='cow.png',
394 media_body=media).execute()
395
396 Depending on the platform you are working on, you may pass -1 as the
397 chunksize, which indicates that the entire file should be uploaded in a single
398 request. If the underlying platform supports streams, such as Python 2.6 or
399 later, then this can be very efficient as it avoids multiple connections, and
400 also avoids loading the entire file into memory before sending it. Note that
401 Google App Engine has a 5MB limit on request size, so you should never set
402 your chunksize larger than 5MB, or to -1.
403 """
404
405 @util.positional(3)
406 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
407 resumable=False):
408 """Constructor.
409
410 Args:
411 fd: io.Base or file object, The source of the bytes to upload. MUST be
412 opened in blocking mode, do not use streams opened in non-blocking mode.
413 The given stream must be seekable, that is, it must be able to call
414 seek() on fd.
415 mimetype: string, Mime-type of the file.
416 chunksize: int, File will be uploaded in chunks of this many bytes. Only
417 used if resumable=True. Pass in a value of -1 if the file is to be
418 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
419 on request size, so you should never set your chunksize larger than 5MB,
420 or to -1.
421 resumable: bool, True if this is a resumable upload. False means upload
422 in a single request.
423 """
424 super(MediaIoBaseUpload, self).__init__()
425 self._fd = fd
426 self._mimetype = mimetype
427 if not (chunksize == -1 or chunksize > 0):
428 raise InvalidChunkSizeError()
429 self._chunksize = chunksize
430 self._resumable = resumable
431
432 self._fd.seek(0, os.SEEK_END)
433 self._size = self._fd.tell()
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:
455 Size of the body, or None of the size is unknown.
456 """
457 return self._size
458
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 shorted than length if EOF was reached
476 first.
477 """
478 self._fd.seek(begin)
479 return self._fd.read(length)
480
481 def has_stream(self):
482 """Does the underlying upload support a streaming interface.
483
484 Streaming means it is an io.IOBase subclass that supports seek, i.e.
485 seekable() returns True.
486
487 Returns:
488 True if the call to stream() will return an instance of a seekable io.Base
489 subclass.
490 """
491 return True
492
493 def stream(self):
494 """A stream interface to the data being uploaded.
495
496 Returns:
497 The returned value is an io.IOBase subclass that supports seek, i.e.
498 seekable() returns True.
499 """
500 return self._fd
501
502 def to_json(self):
503 """This upload type is not serializable."""
504 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
505
506
507class MediaFileUpload(MediaIoBaseUpload):
508 """A MediaUpload for a file.
509
510 Construct a MediaFileUpload and pass as the media_body parameter of the
511 method. For example, if we had a service that allowed uploading images:
512
513
514 media = MediaFileUpload('cow.png', mimetype='image/png',
515 chunksize=1024*1024, resumable=True)
516 farm.animals().insert(
517 id='cow',
518 name='cow.png',
519 media_body=media).execute()
520
521 Depending on the platform you are working on, you may pass -1 as the
522 chunksize, which indicates that the entire file should be uploaded in a single
523 request. If the underlying platform supports streams, such as Python 2.6 or
524 later, then this can be very efficient as it avoids multiple connections, and
525 also avoids loading the entire file into memory before sending it. Note that
526 Google App Engine has a 5MB limit on request size, so you should never set
527 your chunksize larger than 5MB, or to -1.
528 """
529
530 @util.positional(2)
531 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
532 resumable=False):
533 """Constructor.
534
535 Args:
536 filename: string, Name of the file.
537 mimetype: string, Mime-type of the file. If None then a mime-type will be
538 guessed from the file extension.
539 chunksize: int, File will be uploaded in chunks of this many bytes. Only
540 used if resumable=True. Pass in a value of -1 if the file is to be
541 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
542 on request size, so you should never set your chunksize larger than 5MB,
543 or to -1.
544 resumable: bool, True if this is a resumable upload. False means upload
545 in a single request.
546 """
547 self._filename = filename
548 fd = open(self._filename, 'rb')
549 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800550 # No mimetype provided, make a guess.
551 mimetype, _ = mimetypes.guess_type(filename)
552 if mimetype is None:
553 # Guess failed, use octet-stream.
554 mimetype = 'application/octet-stream'
John Asmuth864311d2014-04-24 15:46:08 -0400555 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
556 resumable=resumable)
557
558 def to_json(self):
559 """Creating a JSON representation of an instance of MediaFileUpload.
560
561 Returns:
562 string, a JSON representation of this instance, suitable to pass to
563 from_json().
564 """
565 return self._to_json(strip=['_fd'])
566
567 @staticmethod
568 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700569 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400570 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
571 chunksize=d['_chunksize'], resumable=d['_resumable'])
572
573
574class MediaInMemoryUpload(MediaIoBaseUpload):
575 """MediaUpload for a chunk of bytes.
576
577 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
578 the stream.
579 """
580
581 @util.positional(2)
582 def __init__(self, body, mimetype='application/octet-stream',
583 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
584 """Create a new MediaInMemoryUpload.
585
586 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
587 the stream.
588
589 Args:
590 body: string, Bytes of body content.
591 mimetype: string, Mime-type of the file or default of
592 'application/octet-stream'.
593 chunksize: int, File will be uploaded in chunks of this many bytes. Only
594 used if resumable=True.
595 resumable: bool, True if this is a resumable upload. False means upload
596 in a single request.
597 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800598 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400599 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
600 resumable=resumable)
601
602
603class MediaIoBaseDownload(object):
604 """"Download media resources.
605
606 Note that the Python file object is compatible with io.Base and can be used
607 with this class also.
608
609
610 Example:
611 request = farms.animals().get_media(id='cow')
612 fh = io.FileIO('cow.png', mode='wb')
613 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
614
615 done = False
616 while done is False:
617 status, done = downloader.next_chunk()
618 if status:
619 print "Download %d%%." % int(status.progress() * 100)
620 print "Download Complete!"
621 """
622
623 @util.positional(3)
624 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
625 """Constructor.
626
627 Args:
628 fd: io.Base or file object, The stream in which to write the downloaded
629 bytes.
630 request: googleapiclient.http.HttpRequest, the media request to perform in
631 chunks.
632 chunksize: int, File will be downloaded in chunks of this many bytes.
633 """
634 self._fd = fd
635 self._request = request
636 self._uri = request.uri
637 self._chunksize = chunksize
638 self._progress = 0
639 self._total_size = None
640 self._done = False
641
642 # Stubs for testing.
643 self._sleep = time.sleep
644 self._rand = random.random
645
646 @util.positional(1)
647 def next_chunk(self, num_retries=0):
648 """Get the next chunk of the download.
649
650 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500651 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400652 exponential backoff. If all retries fail, the raised HttpError
653 represents the last request. If zero (default), we attempt the
654 request only once.
655
656 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700657 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400658 The value of 'done' will be True when the media has been fully
659 downloaded.
660
661 Raises:
662 googleapiclient.errors.HttpError if the response was not a 2xx.
663 httplib2.HttpLib2Error if a transport error has occured.
664 """
665 headers = {
666 'range': 'bytes=%d-%d' % (
667 self._progress, self._progress + self._chunksize)
668 }
669 http = self._request.http
670
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100671 resp, content = _retry_request(
672 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
673 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400674
675 if resp.status in [200, 206]:
676 if 'content-location' in resp and resp['content-location'] != self._uri:
677 self._uri = resp['content-location']
678 self._progress += len(content)
679 self._fd.write(content)
680
681 if 'content-range' in resp:
682 content_range = resp['content-range']
683 length = content_range.rsplit('/', 1)[1]
684 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800685 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800686 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400687
688 if self._progress == self._total_size:
689 self._done = True
690 return MediaDownloadProgress(self._progress, self._total_size), self._done
691 else:
692 raise HttpError(resp, content, uri=self._uri)
693
694
695class _StreamSlice(object):
696 """Truncated stream.
697
698 Takes a stream and presents a stream that is a slice of the original stream.
699 This is used when uploading media in chunks. In later versions of Python a
700 stream can be passed to httplib in place of the string of data to send. The
701 problem is that httplib just blindly reads to the end of the stream. This
702 wrapper presents a virtual stream that only reads to the end of the chunk.
703 """
704
705 def __init__(self, stream, begin, chunksize):
706 """Constructor.
707
708 Args:
709 stream: (io.Base, file object), the stream to wrap.
710 begin: int, the seek position the chunk begins at.
711 chunksize: int, the size of the chunk.
712 """
713 self._stream = stream
714 self._begin = begin
715 self._chunksize = chunksize
716 self._stream.seek(begin)
717
718 def read(self, n=-1):
719 """Read n bytes.
720
721 Args:
722 n, int, the number of bytes to read.
723
724 Returns:
725 A string of length 'n', or less if EOF is reached.
726 """
727 # The data left available to read sits in [cur, end)
728 cur = self._stream.tell()
729 end = self._begin + self._chunksize
730 if n == -1 or cur + n > end:
731 n = end - cur
732 return self._stream.read(n)
733
734
735class HttpRequest(object):
736 """Encapsulates a single HTTP request."""
737
738 @util.positional(4)
739 def __init__(self, http, postproc, uri,
740 method='GET',
741 body=None,
742 headers=None,
743 methodId=None,
744 resumable=None):
745 """Constructor for an HttpRequest.
746
747 Args:
748 http: httplib2.Http, the transport object to use to make a request
749 postproc: callable, called on the HTTP response and content to transform
750 it into a data object before returning, or raising an exception
751 on an error.
752 uri: string, the absolute URI to send the request to
753 method: string, the HTTP method to use
754 body: string, the request body of the HTTP request,
755 headers: dict, the HTTP request headers
756 methodId: string, a unique identifier for the API method being called.
757 resumable: MediaUpload, None if this is not a resumbale request.
758 """
759 self.uri = uri
760 self.method = method
761 self.body = body
762 self.headers = headers or {}
763 self.methodId = methodId
764 self.http = http
765 self.postproc = postproc
766 self.resumable = resumable
767 self.response_callbacks = []
768 self._in_error_state = False
769
770 # Pull the multipart boundary out of the content-type header.
771 major, minor, params = mimeparse.parse_mime_type(
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100772 self.headers.get('content-type', 'application/json'))
John Asmuth864311d2014-04-24 15:46:08 -0400773
774 # The size of the non-media part of the request.
775 self.body_size = len(self.body or '')
776
777 # The resumable URI to send chunks to.
778 self.resumable_uri = None
779
780 # The bytes that have been uploaded.
781 self.resumable_progress = 0
782
783 # Stubs for testing.
784 self._rand = random.random
785 self._sleep = time.sleep
786
787 @util.positional(1)
788 def execute(self, http=None, num_retries=0):
789 """Execute the request.
790
791 Args:
792 http: httplib2.Http, an http object to be used in place of the
793 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500794 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400795 exponential backoff. If all retries fail, the raised HttpError
796 represents the last request. If zero (default), we attempt the
797 request only once.
798
799 Returns:
800 A deserialized object model of the response body as determined
801 by the postproc.
802
803 Raises:
804 googleapiclient.errors.HttpError if the response was not a 2xx.
805 httplib2.HttpLib2Error if a transport error has occured.
806 """
807 if http is None:
808 http = self.http
809
810 if self.resumable:
811 body = None
812 while body is None:
813 _, body = self.next_chunk(http=http, num_retries=num_retries)
814 return body
815
816 # Non-resumable case.
817
818 if 'content-length' not in self.headers:
819 self.headers['content-length'] = str(self.body_size)
820 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800821 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400822 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
823 self.method = 'POST'
824 self.headers['x-http-method-override'] = 'GET'
825 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800826 parsed = urlparse(self.uri)
827 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400828 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
829 None)
830 )
831 self.body = parsed.query
832 self.headers['content-length'] = str(len(self.body))
833
834 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100835 resp, content = _retry_request(
836 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
837 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400838
839 for callback in self.response_callbacks:
840 callback(resp)
841 if resp.status >= 300:
842 raise HttpError(resp, content, uri=self.uri)
843 return self.postproc(resp, content)
844
845 @util.positional(2)
846 def add_response_callback(self, cb):
847 """add_response_headers_callback
848
849 Args:
850 cb: Callback to be called on receiving the response headers, of signature:
851
852 def cb(resp):
853 # Where resp is an instance of httplib2.Response
854 """
855 self.response_callbacks.append(cb)
856
857 @util.positional(1)
858 def next_chunk(self, http=None, num_retries=0):
859 """Execute the next step of a resumable upload.
860
861 Can only be used if the method being executed supports media uploads and
862 the MediaUpload object passed in was flagged as using resumable upload.
863
864 Example:
865
866 media = MediaFileUpload('cow.png', mimetype='image/png',
867 chunksize=1000, resumable=True)
868 request = farm.animals().insert(
869 id='cow',
870 name='cow.png',
871 media_body=media)
872
873 response = None
874 while response is None:
875 status, response = request.next_chunk()
876 if status:
877 print "Upload %d%% complete." % int(status.progress() * 100)
878
879
880 Args:
881 http: httplib2.Http, an http object to be used in place of the
882 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500883 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400884 exponential backoff. If all retries fail, the raised HttpError
885 represents the last request. If zero (default), we attempt the
886 request only once.
887
888 Returns:
889 (status, body): (ResumableMediaStatus, object)
890 The body will be None until the resumable media is fully uploaded.
891
892 Raises:
893 googleapiclient.errors.HttpError if the response was not a 2xx.
894 httplib2.HttpLib2Error if a transport error has occured.
895 """
896 if http is None:
897 http = self.http
898
899 if self.resumable.size() is None:
900 size = '*'
901 else:
902 size = str(self.resumable.size())
903
904 if self.resumable_uri is None:
905 start_headers = copy.copy(self.headers)
906 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
907 if size != '*':
908 start_headers['X-Upload-Content-Length'] = size
909 start_headers['content-length'] = str(self.body_size)
910
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100911 resp, content = _retry_request(
912 http, num_retries, 'resumable URI request', self._sleep, self._rand,
913 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400914
915 if resp.status == 200 and 'location' in resp:
916 self.resumable_uri = resp['location']
917 else:
918 raise ResumableUploadError(resp, content)
919 elif self._in_error_state:
920 # If we are in an error state then query the server for current state of
921 # the upload by sending an empty PUT and reading the 'range' header in
922 # the response.
923 headers = {
924 'Content-Range': 'bytes */%s' % size,
925 'content-length': '0'
926 }
927 resp, content = http.request(self.resumable_uri, 'PUT',
928 headers=headers)
929 status, body = self._process_response(resp, content)
930 if body:
931 # The upload was complete.
932 return (status, body)
933
e00Efafe8582015-10-10 18:19:37 +0200934 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400935 data = self.resumable.stream()
936 if self.resumable.chunksize() == -1:
937 data.seek(self.resumable_progress)
938 chunk_end = self.resumable.size() - self.resumable_progress - 1
939 else:
940 # Doing chunking with a stream, so wrap a slice of the stream.
941 data = _StreamSlice(data, self.resumable_progress,
942 self.resumable.chunksize())
943 chunk_end = min(
944 self.resumable_progress + self.resumable.chunksize() - 1,
945 self.resumable.size() - 1)
946 else:
947 data = self.resumable.getbytes(
948 self.resumable_progress, self.resumable.chunksize())
949
950 # A short read implies that we are at EOF, so finish the upload.
951 if len(data) < self.resumable.chunksize():
952 size = str(self.resumable_progress + len(data))
953
954 chunk_end = self.resumable_progress + len(data) - 1
955
956 headers = {
957 'Content-Range': 'bytes %d-%d/%s' % (
958 self.resumable_progress, chunk_end, size),
959 # Must set the content-length header here because httplib can't
960 # calculate the size when working with _StreamSlice.
961 'Content-Length': str(chunk_end - self.resumable_progress + 1)
962 }
963
INADA Naokie4ea1a92015-03-04 03:45:42 +0900964 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400965 if retry_num > 0:
966 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800967 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400968 'Retry #%d for media upload: %s %s, following status: %d'
969 % (retry_num, self.method, self.uri, resp.status))
970
971 try:
972 resp, content = http.request(self.resumable_uri, method='PUT',
973 body=data,
974 headers=headers)
975 except:
976 self._in_error_state = True
977 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500978 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400979 break
980
981 return self._process_response(resp, content)
982
983 def _process_response(self, resp, content):
984 """Process the response from a single chunk upload.
985
986 Args:
987 resp: httplib2.Response, the response object.
988 content: string, the content of the response.
989
990 Returns:
991 (status, body): (ResumableMediaStatus, object)
992 The body will be None until the resumable media is fully uploaded.
993
994 Raises:
995 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
996 """
997 if resp.status in [200, 201]:
998 self._in_error_state = False
999 return None, self.postproc(resp, content)
1000 elif resp.status == 308:
1001 self._in_error_state = False
1002 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001003 try:
1004 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1005 except KeyError:
1006 # If resp doesn't contain range header, resumable progress is 0
1007 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001008 if 'location' in resp:
1009 self.resumable_uri = resp['location']
1010 else:
1011 self._in_error_state = True
1012 raise HttpError(resp, content, uri=self.uri)
1013
1014 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1015 None)
1016
1017 def to_json(self):
1018 """Returns a JSON representation of the HttpRequest."""
1019 d = copy.copy(self.__dict__)
1020 if d['resumable'] is not None:
1021 d['resumable'] = self.resumable.to_json()
1022 del d['http']
1023 del d['postproc']
1024 del d['_sleep']
1025 del d['_rand']
1026
Craig Citro6ae34d72014-08-18 23:10:09 -07001027 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001028
1029 @staticmethod
1030 def from_json(s, http, postproc):
1031 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001032 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001033 if d['resumable'] is not None:
1034 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1035 return HttpRequest(
1036 http,
1037 postproc,
1038 uri=d['uri'],
1039 method=d['method'],
1040 body=d['body'],
1041 headers=d['headers'],
1042 methodId=d['methodId'],
1043 resumable=d['resumable'])
1044
1045
1046class BatchHttpRequest(object):
1047 """Batches multiple HttpRequest objects into a single HTTP request.
1048
1049 Example:
1050 from googleapiclient.http import BatchHttpRequest
1051
1052 def list_animals(request_id, response, exception):
1053 \"\"\"Do something with the animals list response.\"\"\"
1054 if exception is not None:
1055 # Do something with the exception.
1056 pass
1057 else:
1058 # Do something with the response.
1059 pass
1060
1061 def list_farmers(request_id, response, exception):
1062 \"\"\"Do something with the farmers list response.\"\"\"
1063 if exception is not None:
1064 # Do something with the exception.
1065 pass
1066 else:
1067 # Do something with the response.
1068 pass
1069
1070 service = build('farm', 'v2')
1071
1072 batch = BatchHttpRequest()
1073
1074 batch.add(service.animals().list(), list_animals)
1075 batch.add(service.farmers().list(), list_farmers)
1076 batch.execute(http=http)
1077 """
1078
1079 @util.positional(1)
1080 def __init__(self, callback=None, batch_uri=None):
1081 """Constructor for a BatchHttpRequest.
1082
1083 Args:
1084 callback: callable, A callback to be called for each response, of the
1085 form callback(id, response, exception). The first parameter is the
1086 request id, and the second is the deserialized response object. The
1087 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1088 occurred while processing the request, or None if no error occurred.
1089 batch_uri: string, URI to send batch requests to.
1090 """
1091 if batch_uri is None:
1092 batch_uri = 'https://www.googleapis.com/batch'
1093 self._batch_uri = batch_uri
1094
1095 # Global callback to be called for each individual response in the batch.
1096 self._callback = callback
1097
1098 # A map from id to request.
1099 self._requests = {}
1100
1101 # A map from id to callback.
1102 self._callbacks = {}
1103
1104 # List of request ids, in the order in which they were added.
1105 self._order = []
1106
1107 # The last auto generated id.
1108 self._last_auto_id = 0
1109
1110 # Unique ID on which to base the Content-ID headers.
1111 self._base_id = None
1112
1113 # A map from request id to (httplib2.Response, content) response pairs
1114 self._responses = {}
1115
1116 # A map of id(Credentials) that have been refreshed.
1117 self._refreshed_credentials = {}
1118
1119 def _refresh_and_apply_credentials(self, request, http):
1120 """Refresh the credentials and apply to the request.
1121
1122 Args:
1123 request: HttpRequest, the request.
1124 http: httplib2.Http, the global http object for the batch.
1125 """
1126 # For the credentials to refresh, but only once per refresh_token
1127 # If there is no http per the request then refresh the http passed in
1128 # via execute()
1129 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001130 request_credentials = False
1131
1132 if request.http is not None:
1133 creds = _auth.get_credentials_from_http(request.http)
1134 request_credentials = True
1135
1136 if creds is None and http is not None:
1137 creds = _auth.get_credentials_from_http(http)
1138
John Asmuth864311d2014-04-24 15:46:08 -04001139 if creds is not None:
1140 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001141 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001142 self._refreshed_credentials[id(creds)] = 1
1143
1144 # Only apply the credentials if we are using the http object passed in,
1145 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001146 if request.http is None or not request_credentials:
1147 _auth.apply_credentials(creds, request.headers)
1148
John Asmuth864311d2014-04-24 15:46:08 -04001149
1150 def _id_to_header(self, id_):
1151 """Convert an id to a Content-ID header value.
1152
1153 Args:
1154 id_: string, identifier of individual request.
1155
1156 Returns:
1157 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1158 the value because Content-ID headers are supposed to be universally
1159 unique.
1160 """
1161 if self._base_id is None:
1162 self._base_id = uuid.uuid4()
1163
Pat Ferated5b61bd2015-03-03 16:04:11 -08001164 return '<%s+%s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001165
1166 def _header_to_id(self, header):
1167 """Convert a Content-ID header value to an id.
1168
1169 Presumes the Content-ID header conforms to the format that _id_to_header()
1170 returns.
1171
1172 Args:
1173 header: string, Content-ID header value.
1174
1175 Returns:
1176 The extracted id value.
1177
1178 Raises:
1179 BatchError if the header is not in the expected format.
1180 """
1181 if header[0] != '<' or header[-1] != '>':
1182 raise BatchError("Invalid value for Content-ID: %s" % header)
1183 if '+' not in header:
1184 raise BatchError("Invalid value for Content-ID: %s" % header)
1185 base, id_ = header[1:-1].rsplit('+', 1)
1186
Pat Ferated5b61bd2015-03-03 16:04:11 -08001187 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001188
1189 def _serialize_request(self, request):
1190 """Convert an HttpRequest object into a string.
1191
1192 Args:
1193 request: HttpRequest, the request to serialize.
1194
1195 Returns:
1196 The request as a string in application/http format.
1197 """
1198 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001199 parsed = urlparse(request.uri)
1200 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001201 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001202 )
1203 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1204 major, minor = request.headers.get('content-type', 'application/json').split('/')
1205 msg = MIMENonMultipart(major, minor)
1206 headers = request.headers.copy()
1207
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001208 if request.http is not None:
1209 credentials = _auth.get_credentials_from_http(request.http)
1210 if credentials is not None:
1211 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001212
1213 # MIMENonMultipart adds its own Content-Type header.
1214 if 'content-type' in headers:
1215 del headers['content-type']
1216
INADA Naokie4ea1a92015-03-04 03:45:42 +09001217 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001218 msg[key] = value
1219 msg['Host'] = parsed.netloc
1220 msg.set_unixfrom(None)
1221
1222 if request.body is not None:
1223 msg.set_payload(request.body)
1224 msg['content-length'] = str(len(request.body))
1225
1226 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001227 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001228 # maxheaderlen=0 means don't line wrap headers.
1229 g = Generator(fp, maxheaderlen=0)
1230 g.flatten(msg, unixfrom=False)
1231 body = fp.getvalue()
1232
Pat Feratec9abbbd2015-03-03 18:00:38 -08001233 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001234
1235 def _deserialize_response(self, payload):
1236 """Convert string into httplib2 response and content.
1237
1238 Args:
1239 payload: string, headers and body as a string.
1240
1241 Returns:
1242 A pair (resp, content), such as would be returned from httplib2.request.
1243 """
1244 # Strip off the status line
1245 status_line, payload = payload.split('\n', 1)
1246 protocol, status, reason = status_line.split(' ', 2)
1247
1248 # Parse the rest of the response
1249 parser = FeedParser()
1250 parser.feed(payload)
1251 msg = parser.close()
1252 msg['status'] = status
1253
1254 # Create httplib2.Response from the parsed headers.
1255 resp = httplib2.Response(msg)
1256 resp.reason = reason
1257 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1258
1259 content = payload.split('\r\n\r\n', 1)[1]
1260
1261 return resp, content
1262
1263 def _new_id(self):
1264 """Create a new id.
1265
1266 Auto incrementing number that avoids conflicts with ids already used.
1267
1268 Returns:
1269 string, a new unique id.
1270 """
1271 self._last_auto_id += 1
1272 while str(self._last_auto_id) in self._requests:
1273 self._last_auto_id += 1
1274 return str(self._last_auto_id)
1275
1276 @util.positional(2)
1277 def add(self, request, callback=None, request_id=None):
1278 """Add a new request.
1279
1280 Every callback added will be paired with a unique id, the request_id. That
1281 unique id will be passed back to the callback when the response comes back
1282 from the server. The default behavior is to have the library generate it's
1283 own unique id. If the caller passes in a request_id then they must ensure
1284 uniqueness for each request_id, and if they are not an exception is
1285 raised. Callers should either supply all request_ids or nevery supply a
1286 request id, to avoid such an error.
1287
1288 Args:
1289 request: HttpRequest, Request to add to the batch.
1290 callback: callable, A callback to be called for this response, of the
1291 form callback(id, response, exception). The first parameter is the
1292 request id, and the second is the deserialized response object. The
1293 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1294 occurred while processing the request, or None if no errors occurred.
1295 request_id: string, A unique id for the request. The id will be passed to
1296 the callback with the response.
1297
1298 Returns:
1299 None
1300
1301 Raises:
1302 BatchError if a media request is added to a batch.
1303 KeyError is the request_id is not unique.
1304 """
1305 if request_id is None:
1306 request_id = self._new_id()
1307 if request.resumable is not None:
1308 raise BatchError("Media requests cannot be used in a batch request.")
1309 if request_id in self._requests:
1310 raise KeyError("A request with this ID already exists: %s" % request_id)
1311 self._requests[request_id] = request
1312 self._callbacks[request_id] = callback
1313 self._order.append(request_id)
1314
1315 def _execute(self, http, order, requests):
1316 """Serialize batch request, send to server, process response.
1317
1318 Args:
1319 http: httplib2.Http, an http object to be used to make the request with.
1320 order: list, list of request ids in the order they were added to the
1321 batch.
1322 request: list, list of request objects to send.
1323
1324 Raises:
1325 httplib2.HttpLib2Error if a transport error has occured.
1326 googleapiclient.errors.BatchError if the response is the wrong format.
1327 """
1328 message = MIMEMultipart('mixed')
1329 # Message should not write out it's own headers.
1330 setattr(message, '_write_headers', lambda self: None)
1331
1332 # Add all the individual requests.
1333 for request_id in order:
1334 request = requests[request_id]
1335
1336 msg = MIMENonMultipart('application', 'http')
1337 msg['Content-Transfer-Encoding'] = 'binary'
1338 msg['Content-ID'] = self._id_to_header(request_id)
1339
1340 body = self._serialize_request(request)
1341 msg.set_payload(body)
1342 message.attach(msg)
1343
Craig Citro72389b72014-07-15 17:12:50 -07001344 # encode the body: note that we can't use `as_string`, because
1345 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001346 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001347 g = Generator(fp, mangle_from_=False)
1348 g.flatten(message, unixfrom=False)
1349 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001350
1351 headers = {}
1352 headers['content-type'] = ('multipart/mixed; '
1353 'boundary="%s"') % message.get_boundary()
1354
1355 resp, content = http.request(self._batch_uri, method='POST', body=body,
1356 headers=headers)
1357
1358 if resp.status >= 300:
1359 raise HttpError(resp, content, uri=self._batch_uri)
1360
John Asmuth864311d2014-04-24 15:46:08 -04001361 # Prepend with a content-type header so FeedParser can handle it.
1362 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001363 # PY3's FeedParser only accepts unicode. So we should decode content
1364 # here, and encode each payload again.
1365 if six.PY3:
1366 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001367 for_parser = header + content
1368
1369 parser = FeedParser()
1370 parser.feed(for_parser)
1371 mime_response = parser.close()
1372
1373 if not mime_response.is_multipart():
1374 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1375 content=content)
1376
1377 for part in mime_response.get_payload():
1378 request_id = self._header_to_id(part['Content-ID'])
1379 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001380 # We encode content here to emulate normal http response.
1381 if isinstance(content, six.text_type):
1382 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001383 self._responses[request_id] = (response, content)
1384
1385 @util.positional(1)
1386 def execute(self, http=None):
1387 """Execute all the requests as a single batched HTTP request.
1388
1389 Args:
1390 http: httplib2.Http, an http object to be used in place of the one the
1391 HttpRequest request object was constructed with. If one isn't supplied
1392 then use a http object from the requests in this batch.
1393
1394 Returns:
1395 None
1396
1397 Raises:
1398 httplib2.HttpLib2Error if a transport error has occured.
1399 googleapiclient.errors.BatchError if the response is the wrong format.
1400 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001401 # If we have no requests return
1402 if len(self._order) == 0:
1403 return None
John Asmuth864311d2014-04-24 15:46:08 -04001404
1405 # If http is not supplied use the first valid one given in the requests.
1406 if http is None:
1407 for request_id in self._order:
1408 request = self._requests[request_id]
1409 if request is not None:
1410 http = request.http
1411 break
1412
1413 if http is None:
1414 raise ValueError("Missing a valid http object.")
1415
Gabriel Garcia23174be2016-05-25 17:28:07 +02001416 # Special case for OAuth2Credentials-style objects which have not yet been
1417 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001418 creds = _auth.get_credentials_from_http(http)
1419 if creds is not None:
1420 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001421 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001422 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001423
John Asmuth864311d2014-04-24 15:46:08 -04001424 self._execute(http, self._order, self._requests)
1425
1426 # Loop over all the requests and check for 401s. For each 401 request the
1427 # credentials should be refreshed and then sent again in a separate batch.
1428 redo_requests = {}
1429 redo_order = []
1430
1431 for request_id in self._order:
1432 resp, content = self._responses[request_id]
1433 if resp['status'] == '401':
1434 redo_order.append(request_id)
1435 request = self._requests[request_id]
1436 self._refresh_and_apply_credentials(request, http)
1437 redo_requests[request_id] = request
1438
1439 if redo_requests:
1440 self._execute(http, redo_order, redo_requests)
1441
1442 # Now process all callbacks that are erroring, and raise an exception for
1443 # ones that return a non-2xx response? Or add extra parameter to callback
1444 # that contains an HttpError?
1445
1446 for request_id in self._order:
1447 resp, content = self._responses[request_id]
1448
1449 request = self._requests[request_id]
1450 callback = self._callbacks[request_id]
1451
1452 response = None
1453 exception = None
1454 try:
1455 if resp.status >= 300:
1456 raise HttpError(resp, content, uri=request.uri)
1457 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001458 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001459 exception = e
1460
1461 if callback is not None:
1462 callback(request_id, response, exception)
1463 if self._callback is not None:
1464 self._callback(request_id, response, exception)
1465
1466
1467class HttpRequestMock(object):
1468 """Mock of HttpRequest.
1469
1470 Do not construct directly, instead use RequestMockBuilder.
1471 """
1472
1473 def __init__(self, resp, content, postproc):
1474 """Constructor for HttpRequestMock
1475
1476 Args:
1477 resp: httplib2.Response, the response to emulate coming from the request
1478 content: string, the response body
1479 postproc: callable, the post processing function usually supplied by
1480 the model class. See model.JsonModel.response() as an example.
1481 """
1482 self.resp = resp
1483 self.content = content
1484 self.postproc = postproc
1485 if resp is None:
1486 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1487 if 'reason' in self.resp:
1488 self.resp.reason = self.resp['reason']
1489
1490 def execute(self, http=None):
1491 """Execute the request.
1492
1493 Same behavior as HttpRequest.execute(), but the response is
1494 mocked and not really from an HTTP request/response.
1495 """
1496 return self.postproc(self.resp, self.content)
1497
1498
1499class RequestMockBuilder(object):
1500 """A simple mock of HttpRequest
1501
1502 Pass in a dictionary to the constructor that maps request methodIds to
1503 tuples of (httplib2.Response, content, opt_expected_body) that should be
1504 returned when that method is called. None may also be passed in for the
1505 httplib2.Response, in which case a 200 OK response will be generated.
1506 If an opt_expected_body (str or dict) is provided, it will be compared to
1507 the body and UnexpectedBodyError will be raised on inequality.
1508
1509 Example:
1510 response = '{"data": {"id": "tag:google.c...'
1511 requestBuilder = RequestMockBuilder(
1512 {
1513 'plus.activities.get': (None, response),
1514 }
1515 )
1516 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1517
1518 Methods that you do not supply a response for will return a
1519 200 OK with an empty string as the response content or raise an excpetion
1520 if check_unexpected is set to True. The methodId is taken from the rpcName
1521 in the discovery document.
1522
1523 For more details see the project wiki.
1524 """
1525
1526 def __init__(self, responses, check_unexpected=False):
1527 """Constructor for RequestMockBuilder
1528
1529 The constructed object should be a callable object
1530 that can replace the class HttpResponse.
1531
1532 responses - A dictionary that maps methodIds into tuples
1533 of (httplib2.Response, content). The methodId
1534 comes from the 'rpcName' field in the discovery
1535 document.
1536 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1537 should be raised on unsupplied method.
1538 """
1539 self.responses = responses
1540 self.check_unexpected = check_unexpected
1541
1542 def __call__(self, http, postproc, uri, method='GET', body=None,
1543 headers=None, methodId=None, resumable=None):
1544 """Implements the callable interface that discovery.build() expects
1545 of requestBuilder, which is to build an object compatible with
1546 HttpRequest.execute(). See that method for the description of the
1547 parameters and the expected response.
1548 """
1549 if methodId in self.responses:
1550 response = self.responses[methodId]
1551 resp, content = response[:2]
1552 if len(response) > 2:
1553 # Test the body against the supplied expected_body.
1554 expected_body = response[2]
1555 if bool(expected_body) != bool(body):
1556 # Not expecting a body and provided one
1557 # or expecting a body and not provided one.
1558 raise UnexpectedBodyError(expected_body, body)
1559 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001560 expected_body = json.loads(expected_body)
1561 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001562 if body != expected_body:
1563 raise UnexpectedBodyError(expected_body, body)
1564 return HttpRequestMock(resp, content, postproc)
1565 elif self.check_unexpected:
1566 raise UnexpectedMethodError(methodId=methodId)
1567 else:
1568 model = JsonModel(False)
1569 return HttpRequestMock(None, '{}', model.response)
1570
1571
1572class HttpMock(object):
1573 """Mock of httplib2.Http"""
1574
1575 def __init__(self, filename=None, headers=None):
1576 """
1577 Args:
1578 filename: string, absolute filename to read response from
1579 headers: dict, header to return with response
1580 """
1581 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001582 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001583 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001584 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001585 self.data = f.read()
1586 f.close()
1587 else:
1588 self.data = None
1589 self.response_headers = headers
1590 self.headers = None
1591 self.uri = None
1592 self.method = None
1593 self.body = None
1594 self.headers = None
1595
1596
1597 def request(self, uri,
1598 method='GET',
1599 body=None,
1600 headers=None,
1601 redirections=1,
1602 connection_type=None):
1603 self.uri = uri
1604 self.method = method
1605 self.body = body
1606 self.headers = headers
1607 return httplib2.Response(self.response_headers), self.data
1608
1609
1610class HttpMockSequence(object):
1611 """Mock of httplib2.Http
1612
1613 Mocks a sequence of calls to request returning different responses for each
1614 call. Create an instance initialized with the desired response headers
1615 and content and then use as if an httplib2.Http instance.
1616
1617 http = HttpMockSequence([
1618 ({'status': '401'}, ''),
1619 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1620 ({'status': '200'}, 'echo_request_headers'),
1621 ])
1622 resp, content = http.request("http://examples.com")
1623
1624 There are special values you can pass in for content to trigger
1625 behavours that are helpful in testing.
1626
1627 'echo_request_headers' means return the request headers in the response body
1628 'echo_request_headers_as_json' means return the request headers in
1629 the response body
1630 'echo_request_body' means return the request body in the response body
1631 'echo_request_uri' means return the request uri in the response body
1632 """
1633
1634 def __init__(self, iterable):
1635 """
1636 Args:
1637 iterable: iterable, a sequence of pairs of (headers, body)
1638 """
1639 self._iterable = iterable
1640 self.follow_redirects = True
1641
1642 def request(self, uri,
1643 method='GET',
1644 body=None,
1645 headers=None,
1646 redirections=1,
1647 connection_type=None):
1648 resp, content = self._iterable.pop(0)
1649 if content == 'echo_request_headers':
1650 content = headers
1651 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001652 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001653 elif content == 'echo_request_body':
1654 if hasattr(body, 'read'):
1655 content = body.read()
1656 else:
1657 content = body
1658 elif content == 'echo_request_uri':
1659 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001660 if isinstance(content, six.text_type):
1661 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001662 return httplib2.Response(resp), content
1663
1664
1665def set_user_agent(http, user_agent):
1666 """Set the user-agent on every request.
1667
1668 Args:
1669 http - An instance of httplib2.Http
1670 or something that acts like it.
1671 user_agent: string, the value for the user-agent header.
1672
1673 Returns:
1674 A modified instance of http that was passed in.
1675
1676 Example:
1677
1678 h = httplib2.Http()
1679 h = set_user_agent(h, "my-app-name/6.0")
1680
1681 Most of the time the user-agent will be set doing auth, this is for the rare
1682 cases where you are accessing an unauthenticated endpoint.
1683 """
1684 request_orig = http.request
1685
1686 # The closure that will replace 'httplib2.Http.request'.
1687 def new_request(uri, method='GET', body=None, headers=None,
1688 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1689 connection_type=None):
1690 """Modify the request headers to add the user-agent."""
1691 if headers is None:
1692 headers = {}
1693 if 'user-agent' in headers:
1694 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1695 else:
1696 headers['user-agent'] = user_agent
1697 resp, content = request_orig(uri, method, body, headers,
1698 redirections, connection_type)
1699 return resp, content
1700
1701 http.request = new_request
1702 return http
1703
1704
1705def tunnel_patch(http):
1706 """Tunnel PATCH requests over POST.
1707 Args:
1708 http - An instance of httplib2.Http
1709 or something that acts like it.
1710
1711 Returns:
1712 A modified instance of http that was passed in.
1713
1714 Example:
1715
1716 h = httplib2.Http()
1717 h = tunnel_patch(h, "my-app-name/6.0")
1718
1719 Useful if you are running on a platform that doesn't support PATCH.
1720 Apply this last if you are using OAuth 1.0, as changing the method
1721 will result in a different signature.
1722 """
1723 request_orig = http.request
1724
1725 # The closure that will replace 'httplib2.Http.request'.
1726 def new_request(uri, method='GET', body=None, headers=None,
1727 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1728 connection_type=None):
1729 """Modify the request headers to add the user-agent."""
1730 if headers is None:
1731 headers = {}
1732 if method == 'PATCH':
1733 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001734 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001735 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1736 headers['x-http-method-override'] = "PATCH"
1737 method = 'POST'
1738 resp, content = request_orig(uri, method, body, headers,
1739 redirections, connection_type)
1740 return resp, content
1741
1742 http.request = new_request
1743 return http
Igor Maravić22435292017-01-19 22:28:22 +01001744
1745
1746def build_http():
1747 """Builds httplib2.Http object
1748
1749 Returns:
1750 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1751 To override default timeout call
1752
1753 socket.setdefaulttimeout(timeout_in_sec)
1754
1755 before interacting with this method.
1756 """
1757 if socket.getdefaulttimeout() is not None:
1758 http_timeout = socket.getdefaulttimeout()
1759 else:
1760 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1761 return httplib2.Http(timeout=http_timeout)