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