blob: 9733aa3b818a27d151647b2c3773f0b0ed6b09b9 [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
cspeidelfbaf9d72018-05-10 12:50:12 -060019actual HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -040020"""
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
Helen Koikede13e3b2018-04-26 16:05:16 -030058from googleapiclient import _helpers as util
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070059
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070060from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080061from googleapiclient.errors import BatchError
62from googleapiclient.errors import HttpError
63from googleapiclient.errors import InvalidChunkSizeError
64from googleapiclient.errors import ResumableUploadError
65from googleapiclient.errors import UnexpectedBodyError
66from googleapiclient.errors import UnexpectedMethodError
67from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040068
69
Emmett Butler09699152016-02-08 14:26:00 -080070LOGGER = logging.getLogger(__name__)
71
Craig Citrod1aa6912018-02-28 15:31:08 -080072DEFAULT_CHUNK_SIZE = 100*1024*1024
John Asmuth864311d2014-04-24 15:46:08 -040073
74MAX_URI_LENGTH = 2048
75
Xinan Line2dccec2018-12-07 05:28:33 +090076MAX_BATCH_LIMIT = 1000
77
eesheeshc6425a02016-02-12 15:07:06 +000078_TOO_MANY_REQUESTS = 429
79
Igor Maravić22435292017-01-19 22:28:22 +010080DEFAULT_HTTP_TIMEOUT_SEC = 60
81
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070082_LEGACY_BATCH_URI = 'https://www.googleapis.com/batch'
83
eesheeshc6425a02016-02-12 15:07:06 +000084
85def _should_retry_response(resp_status, content):
86 """Determines whether a response should be retried.
87
88 Args:
89 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080090 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000091
92 Returns:
93 True if the response should be retried, otherwise False.
94 """
95 # Retry on 5xx errors.
96 if resp_status >= 500:
97 return True
98
99 # Retry on 429 errors.
100 if resp_status == _TOO_MANY_REQUESTS:
101 return True
102
103 # For 403 errors, we have to check for the `reason` in the response to
104 # determine if we should retry.
105 if resp_status == six.moves.http_client.FORBIDDEN:
106 # If there's no details about the 403 type, don't retry.
107 if not content:
108 return False
109
110 # Content is in JSON format.
111 try:
112 data = json.loads(content.decode('utf-8'))
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -0800113 if isinstance(data, dict):
114 reason = data['error']['errors'][0]['reason']
115 else:
116 reason = data[0]['error']['errors']['reason']
eesheeshc6425a02016-02-12 15:07:06 +0000117 except (UnicodeDecodeError, ValueError, KeyError):
118 LOGGER.warning('Invalid JSON content from response: %s', content)
119 return False
120
121 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
122
123 # Only retry on rate limit related failures.
124 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
125 return True
126
127 # Everything else is a success or non-retriable so break.
128 return False
129
John Asmuth864311d2014-04-24 15:46:08 -0400130
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100131def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
132 **kwargs):
133 """Retries an HTTP request multiple times while handling errors.
134
135 If after all retries the request still fails, last error is either returned as
136 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
137
138 Args:
139 http: Http object to be used to execute request.
140 num_retries: Maximum number of retries.
141 req_type: Type of the request (used for logging retries).
142 sleep, rand: Functions to sleep for random time between retries.
143 uri: URI to be requested.
144 method: HTTP method to be used.
145 args, kwargs: Additional arguments passed to http.request.
146
147 Returns:
148 resp, content - Response from the http request (may be HTTP 5xx).
149 """
150 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000151 content = None
Bu Sun Kim0ba1ca42019-07-23 14:42:53 -0700152 exception = 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,
Bu Sun Kim0ba1ca42019-07-23 14:42:53 -0700160 resp.status if resp else exception)
eesheeshc6425a02016-02-12 15:07:06 +0000161 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
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700169 except socket.timeout as socket_timeout:
170 # It's important that this be before socket.error as it's a subclass
171 # socket.timeout has no errorcode
172 exception = socket_timeout
eesheeshc6425a02016-02-12 15:07:06 +0000173 except socket.error as socket_error:
174 # errno's contents differ by platform, so we have to match by name.
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700175 if socket.errno.errorcode.get(socket_error.errno) not in {
176 'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED'}:
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100177 raise
eesheeshc6425a02016-02-12 15:07:06 +0000178 exception = socket_error
Bashir Sadjadc35150f2018-06-25 11:46:09 -0400179 except httplib2.ServerNotFoundError as server_not_found_error:
180 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000181
182 if exception:
183 if retry_num == num_retries:
184 raise exception
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100185 else:
186 continue
eesheeshc6425a02016-02-12 15:07:06 +0000187
188 if not _should_retry_response(resp.status, content):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100189 break
190
191 return resp, content
192
193
John Asmuth864311d2014-04-24 15:46:08 -0400194class MediaUploadProgress(object):
195 """Status of a resumable upload."""
196
197 def __init__(self, resumable_progress, total_size):
198 """Constructor.
199
200 Args:
201 resumable_progress: int, bytes sent so far.
202 total_size: int, total bytes in complete upload, or None if the total
203 upload size isn't known ahead of time.
204 """
205 self.resumable_progress = resumable_progress
206 self.total_size = total_size
207
208 def progress(self):
209 """Percent of upload completed, as a float.
210
211 Returns:
212 the percentage complete as a float, returning 0.0 if the total size of
213 the upload is unknown.
214 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300215 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400216 return float(self.resumable_progress) / float(self.total_size)
217 else:
218 return 0.0
219
220
221class MediaDownloadProgress(object):
222 """Status of a resumable download."""
223
224 def __init__(self, resumable_progress, total_size):
225 """Constructor.
226
227 Args:
228 resumable_progress: int, bytes received so far.
229 total_size: int, total bytes in complete download.
230 """
231 self.resumable_progress = resumable_progress
232 self.total_size = total_size
233
234 def progress(self):
235 """Percent of download completed, as a float.
236
237 Returns:
238 the percentage complete as a float, returning 0.0 if the total size of
239 the download is unknown.
240 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300241 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400242 return float(self.resumable_progress) / float(self.total_size)
243 else:
244 return 0.0
245
246
247class MediaUpload(object):
248 """Describes a media object to upload.
249
250 Base class that defines the interface of MediaUpload subclasses.
251
252 Note that subclasses of MediaUpload may allow you to control the chunksize
253 when uploading a media object. It is important to keep the size of the chunk
254 as large as possible to keep the upload efficient. Other factors may influence
255 the size of the chunk you use, particularly if you are working in an
256 environment where individual HTTP requests may have a hardcoded time limit,
257 such as under certain classes of requests under Google App Engine.
258
259 Streams are io.Base compatible objects that support seek(). Some MediaUpload
260 subclasses support using streams directly to upload data. Support for
261 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
262 platform that stream will be used for uploading the media object. The support
263 for streaming is indicated by has_stream() returning True. The stream() method
264 should return an io.Base object that supports seek(). On platforms where the
265 underlying httplib module supports streaming, for example Python 2.6 and
266 later, the stream will be passed into the http library which will result in
267 less memory being used and possibly faster uploads.
268
269 If you need to upload media that can't be uploaded using any of the existing
270 MediaUpload sub-class then you can sub-class MediaUpload for your particular
271 needs.
272 """
273
274 def chunksize(self):
275 """Chunk size for resumable uploads.
276
277 Returns:
278 Chunk size in bytes.
279 """
280 raise NotImplementedError()
281
282 def mimetype(self):
283 """Mime type of the body.
284
285 Returns:
286 Mime type.
287 """
288 return 'application/octet-stream'
289
290 def size(self):
291 """Size of upload.
292
293 Returns:
294 Size of the body, or None of the size is unknown.
295 """
296 return None
297
298 def resumable(self):
299 """Whether this upload is resumable.
300
301 Returns:
302 True if resumable upload or False.
303 """
304 return False
305
306 def getbytes(self, begin, end):
307 """Get bytes from the media.
308
309 Args:
310 begin: int, offset from beginning of file.
311 length: int, number of bytes to read, starting at begin.
312
313 Returns:
314 A string of bytes read. May be shorter than length if EOF was reached
315 first.
316 """
317 raise NotImplementedError()
318
319 def has_stream(self):
320 """Does the underlying upload support a streaming interface.
321
322 Streaming means it is an io.IOBase subclass that supports seek, i.e.
323 seekable() returns True.
324
325 Returns:
326 True if the call to stream() will return an instance of a seekable io.Base
327 subclass.
328 """
329 return False
330
331 def stream(self):
332 """A stream interface to the data being uploaded.
333
334 Returns:
335 The returned value is an io.IOBase subclass that supports seek, i.e.
336 seekable() returns True.
337 """
338 raise NotImplementedError()
339
340 @util.positional(1)
341 def _to_json(self, strip=None):
342 """Utility function for creating a JSON representation of a MediaUpload.
343
344 Args:
345 strip: array, An array of names of members to not include in the JSON.
346
347 Returns:
348 string, a JSON representation of this instance, suitable to pass to
349 from_json().
350 """
351 t = type(self)
352 d = copy.copy(self.__dict__)
353 if strip is not None:
354 for member in strip:
355 del d[member]
356 d['_class'] = t.__name__
357 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700358 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400359
360 def to_json(self):
361 """Create a JSON representation of an instance of MediaUpload.
362
363 Returns:
364 string, a JSON representation of this instance, suitable to pass to
365 from_json().
366 """
367 return self._to_json()
368
369 @classmethod
370 def new_from_json(cls, s):
371 """Utility class method to instantiate a MediaUpload subclass from a JSON
372 representation produced by to_json().
373
374 Args:
375 s: string, JSON from to_json().
376
377 Returns:
378 An instance of the subclass of MediaUpload that was serialized with
379 to_json().
380 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700381 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400382 # Find and call the right classmethod from_json() to restore the object.
383 module = data['_module']
384 m = __import__(module, fromlist=module.split('.')[:-1])
385 kls = getattr(m, data['_class'])
386 from_json = getattr(kls, 'from_json')
387 return from_json(s)
388
389
390class MediaIoBaseUpload(MediaUpload):
391 """A MediaUpload for a io.Base objects.
392
393 Note that the Python file object is compatible with io.Base and can be used
394 with this class also.
395
Pat Ferateed9affd2015-03-03 16:03:15 -0800396 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400397 media = MediaIoBaseUpload(fh, mimetype='image/png',
398 chunksize=1024*1024, resumable=True)
399 farm.animals().insert(
400 id='cow',
401 name='cow.png',
402 media_body=media).execute()
403
404 Depending on the platform you are working on, you may pass -1 as the
405 chunksize, which indicates that the entire file should be uploaded in a single
406 request. If the underlying platform supports streams, such as Python 2.6 or
407 later, then this can be very efficient as it avoids multiple connections, and
408 also avoids loading the entire file into memory before sending it. Note that
409 Google App Engine has a 5MB limit on request size, so you should never set
410 your chunksize larger than 5MB, or to -1.
411 """
412
413 @util.positional(3)
414 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
415 resumable=False):
416 """Constructor.
417
418 Args:
419 fd: io.Base or file object, The source of the bytes to upload. MUST be
420 opened in blocking mode, do not use streams opened in non-blocking mode.
421 The given stream must be seekable, that is, it must be able to call
422 seek() on fd.
423 mimetype: string, Mime-type of the file.
424 chunksize: int, File will be uploaded in chunks of this many bytes. Only
425 used if resumable=True. Pass in a value of -1 if the file is to be
426 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
427 on request size, so you should never set your chunksize larger than 5MB,
428 or to -1.
429 resumable: bool, True if this is a resumable upload. False means upload
430 in a single request.
431 """
432 super(MediaIoBaseUpload, self).__init__()
433 self._fd = fd
434 self._mimetype = mimetype
435 if not (chunksize == -1 or chunksize > 0):
436 raise InvalidChunkSizeError()
437 self._chunksize = chunksize
438 self._resumable = resumable
439
440 self._fd.seek(0, os.SEEK_END)
441 self._size = self._fd.tell()
442
443 def chunksize(self):
444 """Chunk size for resumable uploads.
445
446 Returns:
447 Chunk size in bytes.
448 """
449 return self._chunksize
450
451 def mimetype(self):
452 """Mime type of the body.
453
454 Returns:
455 Mime type.
456 """
457 return self._mimetype
458
459 def size(self):
460 """Size of upload.
461
462 Returns:
463 Size of the body, or None of the size is unknown.
464 """
465 return self._size
466
467 def resumable(self):
468 """Whether this upload is resumable.
469
470 Returns:
471 True if resumable upload or False.
472 """
473 return self._resumable
474
475 def getbytes(self, begin, length):
476 """Get bytes from the media.
477
478 Args:
479 begin: int, offset from beginning of file.
480 length: int, number of bytes to read, starting at begin.
481
482 Returns:
483 A string of bytes read. May be shorted than length if EOF was reached
484 first.
485 """
486 self._fd.seek(begin)
487 return self._fd.read(length)
488
489 def has_stream(self):
490 """Does the underlying upload support a streaming interface.
491
492 Streaming means it is an io.IOBase subclass that supports seek, i.e.
493 seekable() returns True.
494
495 Returns:
496 True if the call to stream() will return an instance of a seekable io.Base
497 subclass.
498 """
499 return True
500
501 def stream(self):
502 """A stream interface to the data being uploaded.
503
504 Returns:
505 The returned value is an io.IOBase subclass that supports seek, i.e.
506 seekable() returns True.
507 """
508 return self._fd
509
510 def to_json(self):
511 """This upload type is not serializable."""
512 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
513
514
515class MediaFileUpload(MediaIoBaseUpload):
516 """A MediaUpload for a file.
517
518 Construct a MediaFileUpload and pass as the media_body parameter of the
519 method. For example, if we had a service that allowed uploading images:
520
John Asmuth864311d2014-04-24 15:46:08 -0400521 media = MediaFileUpload('cow.png', mimetype='image/png',
522 chunksize=1024*1024, resumable=True)
523 farm.animals().insert(
524 id='cow',
525 name='cow.png',
526 media_body=media).execute()
527
528 Depending on the platform you are working on, you may pass -1 as the
529 chunksize, which indicates that the entire file should be uploaded in a single
530 request. If the underlying platform supports streams, such as Python 2.6 or
531 later, then this can be very efficient as it avoids multiple connections, and
532 also avoids loading the entire file into memory before sending it. Note that
533 Google App Engine has a 5MB limit on request size, so you should never set
534 your chunksize larger than 5MB, or to -1.
535 """
536
537 @util.positional(2)
538 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
539 resumable=False):
540 """Constructor.
541
542 Args:
543 filename: string, Name of the file.
544 mimetype: string, Mime-type of the file. If None then a mime-type will be
545 guessed from the file extension.
546 chunksize: int, File will be uploaded in chunks of this many bytes. Only
547 used if resumable=True. Pass in a value of -1 if the file is to be
548 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
549 on request size, so you should never set your chunksize larger than 5MB,
550 or to -1.
551 resumable: bool, True if this is a resumable upload. False means upload
552 in a single request.
553 """
554 self._filename = filename
555 fd = open(self._filename, 'rb')
556 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800557 # No mimetype provided, make a guess.
558 mimetype, _ = mimetypes.guess_type(filename)
559 if mimetype is None:
560 # Guess failed, use octet-stream.
561 mimetype = 'application/octet-stream'
Xiaofei Wang20b67582019-07-17 11:16:53 -0700562 super(MediaFileUpload, self).__init__(fd, mimetype,
563 chunksize=chunksize,
John Asmuth864311d2014-04-24 15:46:08 -0400564 resumable=resumable)
565
Xiaofei Wang20b67582019-07-17 11:16:53 -0700566 def __del__(self):
567 self._fd.close()
568
John Asmuth864311d2014-04-24 15:46:08 -0400569 def to_json(self):
570 """Creating a JSON representation of an instance of MediaFileUpload.
571
572 Returns:
573 string, a JSON representation of this instance, suitable to pass to
574 from_json().
575 """
576 return self._to_json(strip=['_fd'])
577
578 @staticmethod
579 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700580 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400581 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
582 chunksize=d['_chunksize'], resumable=d['_resumable'])
583
584
585class MediaInMemoryUpload(MediaIoBaseUpload):
586 """MediaUpload for a chunk of bytes.
587
588 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
589 the stream.
590 """
591
592 @util.positional(2)
593 def __init__(self, body, mimetype='application/octet-stream',
594 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
595 """Create a new MediaInMemoryUpload.
596
597 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
598 the stream.
599
600 Args:
601 body: string, Bytes of body content.
602 mimetype: string, Mime-type of the file or default of
603 'application/octet-stream'.
604 chunksize: int, File will be uploaded in chunks of this many bytes. Only
605 used if resumable=True.
606 resumable: bool, True if this is a resumable upload. False means upload
607 in a single request.
608 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800609 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400610 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
611 resumable=resumable)
612
613
614class MediaIoBaseDownload(object):
615 """"Download media resources.
616
617 Note that the Python file object is compatible with io.Base and can be used
618 with this class also.
619
620
621 Example:
622 request = farms.animals().get_media(id='cow')
623 fh = io.FileIO('cow.png', mode='wb')
624 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
625
626 done = False
627 while done is False:
628 status, done = downloader.next_chunk()
629 if status:
630 print "Download %d%%." % int(status.progress() * 100)
631 print "Download Complete!"
632 """
633
634 @util.positional(3)
635 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
636 """Constructor.
637
638 Args:
639 fd: io.Base or file object, The stream in which to write the downloaded
640 bytes.
641 request: googleapiclient.http.HttpRequest, the media request to perform in
642 chunks.
643 chunksize: int, File will be downloaded in chunks of this many bytes.
644 """
645 self._fd = fd
646 self._request = request
647 self._uri = request.uri
648 self._chunksize = chunksize
649 self._progress = 0
650 self._total_size = None
651 self._done = False
652
653 # Stubs for testing.
654 self._sleep = time.sleep
655 self._rand = random.random
656
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400657 self._headers = {}
658 for k, v in six.iteritems(request.headers):
659 # allow users to supply custom headers by setting them on the request
660 # but strip out the ones that are set by default on requests generated by
661 # API methods like Drive's files().get(fileId=...)
662 if not k.lower() in ('accept', 'accept-encoding', 'user-agent'):
663 self._headers[k] = v
664
John Asmuth864311d2014-04-24 15:46:08 -0400665 @util.positional(1)
666 def next_chunk(self, num_retries=0):
667 """Get the next chunk of the download.
668
669 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500670 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400671 exponential backoff. If all retries fail, the raised HttpError
672 represents the last request. If zero (default), we attempt the
673 request only once.
674
675 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700676 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400677 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100678 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400679
680 Raises:
681 googleapiclient.errors.HttpError if the response was not a 2xx.
682 httplib2.HttpLib2Error if a transport error has occured.
683 """
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400684 headers = self._headers.copy()
685 headers['range'] = 'bytes=%d-%d' % (
John Asmuth864311d2014-04-24 15:46:08 -0400686 self._progress, self._progress + self._chunksize)
John Asmuth864311d2014-04-24 15:46:08 -0400687 http = self._request.http
688
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100689 resp, content = _retry_request(
690 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
691 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400692
693 if resp.status in [200, 206]:
694 if 'content-location' in resp and resp['content-location'] != self._uri:
695 self._uri = resp['content-location']
696 self._progress += len(content)
697 self._fd.write(content)
698
699 if 'content-range' in resp:
700 content_range = resp['content-range']
701 length = content_range.rsplit('/', 1)[1]
702 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800703 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800704 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400705
Daniel44067782018-01-16 23:17:56 +0100706 if self._total_size is None or self._progress == self._total_size:
John Asmuth864311d2014-04-24 15:46:08 -0400707 self._done = True
708 return MediaDownloadProgress(self._progress, self._total_size), self._done
709 else:
710 raise HttpError(resp, content, uri=self._uri)
711
712
713class _StreamSlice(object):
714 """Truncated stream.
715
716 Takes a stream and presents a stream that is a slice of the original stream.
717 This is used when uploading media in chunks. In later versions of Python a
718 stream can be passed to httplib in place of the string of data to send. The
719 problem is that httplib just blindly reads to the end of the stream. This
720 wrapper presents a virtual stream that only reads to the end of the chunk.
721 """
722
723 def __init__(self, stream, begin, chunksize):
724 """Constructor.
725
726 Args:
727 stream: (io.Base, file object), the stream to wrap.
728 begin: int, the seek position the chunk begins at.
729 chunksize: int, the size of the chunk.
730 """
731 self._stream = stream
732 self._begin = begin
733 self._chunksize = chunksize
734 self._stream.seek(begin)
735
736 def read(self, n=-1):
737 """Read n bytes.
738
739 Args:
740 n, int, the number of bytes to read.
741
742 Returns:
743 A string of length 'n', or less if EOF is reached.
744 """
745 # The data left available to read sits in [cur, end)
746 cur = self._stream.tell()
747 end = self._begin + self._chunksize
748 if n == -1 or cur + n > end:
749 n = end - cur
750 return self._stream.read(n)
751
752
753class HttpRequest(object):
754 """Encapsulates a single HTTP request."""
755
756 @util.positional(4)
757 def __init__(self, http, postproc, uri,
758 method='GET',
759 body=None,
760 headers=None,
761 methodId=None,
762 resumable=None):
763 """Constructor for an HttpRequest.
764
765 Args:
766 http: httplib2.Http, the transport object to use to make a request
767 postproc: callable, called on the HTTP response and content to transform
768 it into a data object before returning, or raising an exception
769 on an error.
770 uri: string, the absolute URI to send the request to
771 method: string, the HTTP method to use
772 body: string, the request body of the HTTP request,
773 headers: dict, the HTTP request headers
774 methodId: string, a unique identifier for the API method being called.
775 resumable: MediaUpload, None if this is not a resumbale request.
776 """
777 self.uri = uri
778 self.method = method
779 self.body = body
780 self.headers = headers or {}
781 self.methodId = methodId
782 self.http = http
783 self.postproc = postproc
784 self.resumable = resumable
785 self.response_callbacks = []
786 self._in_error_state = False
787
John Asmuth864311d2014-04-24 15:46:08 -0400788 # The size of the non-media part of the request.
789 self.body_size = len(self.body or '')
790
791 # The resumable URI to send chunks to.
792 self.resumable_uri = None
793
794 # The bytes that have been uploaded.
795 self.resumable_progress = 0
796
797 # Stubs for testing.
798 self._rand = random.random
799 self._sleep = time.sleep
800
801 @util.positional(1)
802 def execute(self, http=None, num_retries=0):
803 """Execute the request.
804
805 Args:
806 http: httplib2.Http, an http object to be used in place of the
807 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500808 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400809 exponential backoff. If all retries fail, the raised HttpError
810 represents the last request. If zero (default), we attempt the
811 request only once.
812
813 Returns:
814 A deserialized object model of the response body as determined
815 by the postproc.
816
817 Raises:
818 googleapiclient.errors.HttpError if the response was not a 2xx.
819 httplib2.HttpLib2Error if a transport error has occured.
820 """
821 if http is None:
822 http = self.http
823
824 if self.resumable:
825 body = None
826 while body is None:
827 _, body = self.next_chunk(http=http, num_retries=num_retries)
828 return body
829
830 # Non-resumable case.
831
832 if 'content-length' not in self.headers:
833 self.headers['content-length'] = str(self.body_size)
834 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800835 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400836 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
837 self.method = 'POST'
838 self.headers['x-http-method-override'] = 'GET'
839 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800840 parsed = urlparse(self.uri)
841 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400842 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
843 None)
844 )
845 self.body = parsed.query
846 self.headers['content-length'] = str(len(self.body))
847
848 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100849 resp, content = _retry_request(
850 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
851 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400852
853 for callback in self.response_callbacks:
854 callback(resp)
855 if resp.status >= 300:
856 raise HttpError(resp, content, uri=self.uri)
857 return self.postproc(resp, content)
858
859 @util.positional(2)
860 def add_response_callback(self, cb):
861 """add_response_headers_callback
862
863 Args:
864 cb: Callback to be called on receiving the response headers, of signature:
865
866 def cb(resp):
867 # Where resp is an instance of httplib2.Response
868 """
869 self.response_callbacks.append(cb)
870
871 @util.positional(1)
872 def next_chunk(self, http=None, num_retries=0):
873 """Execute the next step of a resumable upload.
874
875 Can only be used if the method being executed supports media uploads and
876 the MediaUpload object passed in was flagged as using resumable upload.
877
878 Example:
879
880 media = MediaFileUpload('cow.png', mimetype='image/png',
881 chunksize=1000, resumable=True)
882 request = farm.animals().insert(
883 id='cow',
884 name='cow.png',
885 media_body=media)
886
887 response = None
888 while response is None:
889 status, response = request.next_chunk()
890 if status:
891 print "Upload %d%% complete." % int(status.progress() * 100)
892
893
894 Args:
895 http: httplib2.Http, an http object to be used in place of the
896 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500897 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400898 exponential backoff. If all retries fail, the raised HttpError
899 represents the last request. If zero (default), we attempt the
900 request only once.
901
902 Returns:
903 (status, body): (ResumableMediaStatus, object)
904 The body will be None until the resumable media is fully uploaded.
905
906 Raises:
907 googleapiclient.errors.HttpError if the response was not a 2xx.
908 httplib2.HttpLib2Error if a transport error has occured.
909 """
910 if http is None:
911 http = self.http
912
913 if self.resumable.size() is None:
914 size = '*'
915 else:
916 size = str(self.resumable.size())
917
918 if self.resumable_uri is None:
919 start_headers = copy.copy(self.headers)
920 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
921 if size != '*':
922 start_headers['X-Upload-Content-Length'] = size
923 start_headers['content-length'] = str(self.body_size)
924
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100925 resp, content = _retry_request(
926 http, num_retries, 'resumable URI request', self._sleep, self._rand,
927 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400928
929 if resp.status == 200 and 'location' in resp:
930 self.resumable_uri = resp['location']
931 else:
932 raise ResumableUploadError(resp, content)
933 elif self._in_error_state:
934 # If we are in an error state then query the server for current state of
935 # the upload by sending an empty PUT and reading the 'range' header in
936 # the response.
937 headers = {
938 'Content-Range': 'bytes */%s' % size,
939 'content-length': '0'
940 }
941 resp, content = http.request(self.resumable_uri, 'PUT',
942 headers=headers)
943 status, body = self._process_response(resp, content)
944 if body:
945 # The upload was complete.
946 return (status, body)
947
e00Efafe8582015-10-10 18:19:37 +0200948 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400949 data = self.resumable.stream()
950 if self.resumable.chunksize() == -1:
951 data.seek(self.resumable_progress)
952 chunk_end = self.resumable.size() - self.resumable_progress - 1
953 else:
954 # Doing chunking with a stream, so wrap a slice of the stream.
955 data = _StreamSlice(data, self.resumable_progress,
956 self.resumable.chunksize())
957 chunk_end = min(
958 self.resumable_progress + self.resumable.chunksize() - 1,
959 self.resumable.size() - 1)
960 else:
961 data = self.resumable.getbytes(
962 self.resumable_progress, self.resumable.chunksize())
963
964 # A short read implies that we are at EOF, so finish the upload.
965 if len(data) < self.resumable.chunksize():
966 size = str(self.resumable_progress + len(data))
967
968 chunk_end = self.resumable_progress + len(data) - 1
969
970 headers = {
971 'Content-Range': 'bytes %d-%d/%s' % (
972 self.resumable_progress, chunk_end, size),
973 # Must set the content-length header here because httplib can't
974 # calculate the size when working with _StreamSlice.
975 'Content-Length': str(chunk_end - self.resumable_progress + 1)
976 }
977
INADA Naokie4ea1a92015-03-04 03:45:42 +0900978 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400979 if retry_num > 0:
980 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800981 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400982 'Retry #%d for media upload: %s %s, following status: %d'
983 % (retry_num, self.method, self.uri, resp.status))
984
985 try:
986 resp, content = http.request(self.resumable_uri, method='PUT',
987 body=data,
988 headers=headers)
989 except:
990 self._in_error_state = True
991 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500992 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400993 break
994
995 return self._process_response(resp, content)
996
997 def _process_response(self, resp, content):
998 """Process the response from a single chunk upload.
999
1000 Args:
1001 resp: httplib2.Response, the response object.
1002 content: string, the content of the response.
1003
1004 Returns:
1005 (status, body): (ResumableMediaStatus, object)
1006 The body will be None until the resumable media is fully uploaded.
1007
1008 Raises:
1009 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1010 """
1011 if resp.status in [200, 201]:
1012 self._in_error_state = False
1013 return None, self.postproc(resp, content)
1014 elif resp.status == 308:
1015 self._in_error_state = False
1016 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001017 try:
1018 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1019 except KeyError:
1020 # If resp doesn't contain range header, resumable progress is 0
1021 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001022 if 'location' in resp:
1023 self.resumable_uri = resp['location']
1024 else:
1025 self._in_error_state = True
1026 raise HttpError(resp, content, uri=self.uri)
1027
1028 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1029 None)
1030
1031 def to_json(self):
1032 """Returns a JSON representation of the HttpRequest."""
1033 d = copy.copy(self.__dict__)
1034 if d['resumable'] is not None:
1035 d['resumable'] = self.resumable.to_json()
1036 del d['http']
1037 del d['postproc']
1038 del d['_sleep']
1039 del d['_rand']
1040
Craig Citro6ae34d72014-08-18 23:10:09 -07001041 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001042
1043 @staticmethod
1044 def from_json(s, http, postproc):
1045 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001046 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001047 if d['resumable'] is not None:
1048 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1049 return HttpRequest(
1050 http,
1051 postproc,
1052 uri=d['uri'],
1053 method=d['method'],
1054 body=d['body'],
1055 headers=d['headers'],
1056 methodId=d['methodId'],
1057 resumable=d['resumable'])
1058
1059
1060class BatchHttpRequest(object):
1061 """Batches multiple HttpRequest objects into a single HTTP request.
1062
1063 Example:
1064 from googleapiclient.http import BatchHttpRequest
1065
1066 def list_animals(request_id, response, exception):
1067 \"\"\"Do something with the animals list response.\"\"\"
1068 if exception is not None:
1069 # Do something with the exception.
1070 pass
1071 else:
1072 # Do something with the response.
1073 pass
1074
1075 def list_farmers(request_id, response, exception):
1076 \"\"\"Do something with the farmers list response.\"\"\"
1077 if exception is not None:
1078 # Do something with the exception.
1079 pass
1080 else:
1081 # Do something with the response.
1082 pass
1083
1084 service = build('farm', 'v2')
1085
1086 batch = BatchHttpRequest()
1087
1088 batch.add(service.animals().list(), list_animals)
1089 batch.add(service.farmers().list(), list_farmers)
1090 batch.execute(http=http)
1091 """
1092
1093 @util.positional(1)
1094 def __init__(self, callback=None, batch_uri=None):
1095 """Constructor for a BatchHttpRequest.
1096
1097 Args:
1098 callback: callable, A callback to be called for each response, of the
1099 form callback(id, response, exception). The first parameter is the
1100 request id, and the second is the deserialized response object. The
1101 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1102 occurred while processing the request, or None if no error occurred.
1103 batch_uri: string, URI to send batch requests to.
1104 """
1105 if batch_uri is None:
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001106 batch_uri = _LEGACY_BATCH_URI
1107
1108 if batch_uri == _LEGACY_BATCH_URI:
1109 LOGGER.warn(
1110 "You have constructed a BatchHttpRequest using the legacy batch "
1111 "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1112 "Please provide the API-specific endpoint or use "
1113 "service.new_batch_http_request(). For more details see "
1114 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1115 "and https://developers.google.com/api-client-library/python/guide/batch.",
1116 _LEGACY_BATCH_URI)
John Asmuth864311d2014-04-24 15:46:08 -04001117 self._batch_uri = batch_uri
1118
1119 # Global callback to be called for each individual response in the batch.
1120 self._callback = callback
1121
1122 # A map from id to request.
1123 self._requests = {}
1124
1125 # A map from id to callback.
1126 self._callbacks = {}
1127
1128 # List of request ids, in the order in which they were added.
1129 self._order = []
1130
1131 # The last auto generated id.
1132 self._last_auto_id = 0
1133
1134 # Unique ID on which to base the Content-ID headers.
1135 self._base_id = None
1136
1137 # A map from request id to (httplib2.Response, content) response pairs
1138 self._responses = {}
1139
1140 # A map of id(Credentials) that have been refreshed.
1141 self._refreshed_credentials = {}
1142
1143 def _refresh_and_apply_credentials(self, request, http):
1144 """Refresh the credentials and apply to the request.
1145
1146 Args:
1147 request: HttpRequest, the request.
1148 http: httplib2.Http, the global http object for the batch.
1149 """
1150 # For the credentials to refresh, but only once per refresh_token
1151 # If there is no http per the request then refresh the http passed in
1152 # via execute()
1153 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001154 request_credentials = False
1155
1156 if request.http is not None:
1157 creds = _auth.get_credentials_from_http(request.http)
1158 request_credentials = True
1159
1160 if creds is None and http is not None:
1161 creds = _auth.get_credentials_from_http(http)
1162
John Asmuth864311d2014-04-24 15:46:08 -04001163 if creds is not None:
1164 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001165 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001166 self._refreshed_credentials[id(creds)] = 1
1167
1168 # Only apply the credentials if we are using the http object passed in,
1169 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001170 if request.http is None or not request_credentials:
1171 _auth.apply_credentials(creds, request.headers)
1172
John Asmuth864311d2014-04-24 15:46:08 -04001173
1174 def _id_to_header(self, id_):
1175 """Convert an id to a Content-ID header value.
1176
1177 Args:
1178 id_: string, identifier of individual request.
1179
1180 Returns:
1181 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1182 the value because Content-ID headers are supposed to be universally
1183 unique.
1184 """
1185 if self._base_id is None:
1186 self._base_id = uuid.uuid4()
1187
Chris McDonough3cf5e602018-07-18 16:18:38 -04001188 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1189 # line folding works properly on Python 3; see
1190 # https://github.com/google/google-api-python-client/issues/164
1191 return '<%s + %s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001192
1193 def _header_to_id(self, header):
1194 """Convert a Content-ID header value to an id.
1195
1196 Presumes the Content-ID header conforms to the format that _id_to_header()
1197 returns.
1198
1199 Args:
1200 header: string, Content-ID header value.
1201
1202 Returns:
1203 The extracted id value.
1204
1205 Raises:
1206 BatchError if the header is not in the expected format.
1207 """
1208 if header[0] != '<' or header[-1] != '>':
1209 raise BatchError("Invalid value for Content-ID: %s" % header)
1210 if '+' not in header:
1211 raise BatchError("Invalid value for Content-ID: %s" % header)
Chris McDonough3cf5e602018-07-18 16:18:38 -04001212 base, id_ = header[1:-1].split(' + ', 1)
John Asmuth864311d2014-04-24 15:46:08 -04001213
Pat Ferated5b61bd2015-03-03 16:04:11 -08001214 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001215
1216 def _serialize_request(self, request):
1217 """Convert an HttpRequest object into a string.
1218
1219 Args:
1220 request: HttpRequest, the request to serialize.
1221
1222 Returns:
1223 The request as a string in application/http format.
1224 """
1225 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001226 parsed = urlparse(request.uri)
1227 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001228 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001229 )
1230 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1231 major, minor = request.headers.get('content-type', 'application/json').split('/')
1232 msg = MIMENonMultipart(major, minor)
1233 headers = request.headers.copy()
1234
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001235 if request.http is not None:
1236 credentials = _auth.get_credentials_from_http(request.http)
1237 if credentials is not None:
1238 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001239
1240 # MIMENonMultipart adds its own Content-Type header.
1241 if 'content-type' in headers:
1242 del headers['content-type']
1243
INADA Naokie4ea1a92015-03-04 03:45:42 +09001244 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001245 msg[key] = value
1246 msg['Host'] = parsed.netloc
1247 msg.set_unixfrom(None)
1248
1249 if request.body is not None:
1250 msg.set_payload(request.body)
1251 msg['content-length'] = str(len(request.body))
1252
1253 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001254 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001255 # maxheaderlen=0 means don't line wrap headers.
1256 g = Generator(fp, maxheaderlen=0)
1257 g.flatten(msg, unixfrom=False)
1258 body = fp.getvalue()
1259
Pat Feratec9abbbd2015-03-03 18:00:38 -08001260 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001261
1262 def _deserialize_response(self, payload):
1263 """Convert string into httplib2 response and content.
1264
1265 Args:
1266 payload: string, headers and body as a string.
1267
1268 Returns:
1269 A pair (resp, content), such as would be returned from httplib2.request.
1270 """
1271 # Strip off the status line
1272 status_line, payload = payload.split('\n', 1)
1273 protocol, status, reason = status_line.split(' ', 2)
1274
1275 # Parse the rest of the response
1276 parser = FeedParser()
1277 parser.feed(payload)
1278 msg = parser.close()
1279 msg['status'] = status
1280
1281 # Create httplib2.Response from the parsed headers.
1282 resp = httplib2.Response(msg)
1283 resp.reason = reason
1284 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1285
1286 content = payload.split('\r\n\r\n', 1)[1]
1287
1288 return resp, content
1289
1290 def _new_id(self):
1291 """Create a new id.
1292
1293 Auto incrementing number that avoids conflicts with ids already used.
1294
1295 Returns:
1296 string, a new unique id.
1297 """
1298 self._last_auto_id += 1
1299 while str(self._last_auto_id) in self._requests:
1300 self._last_auto_id += 1
1301 return str(self._last_auto_id)
1302
1303 @util.positional(2)
1304 def add(self, request, callback=None, request_id=None):
1305 """Add a new request.
1306
1307 Every callback added will be paired with a unique id, the request_id. That
1308 unique id will be passed back to the callback when the response comes back
1309 from the server. The default behavior is to have the library generate it's
1310 own unique id. If the caller passes in a request_id then they must ensure
1311 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001312 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001313 request id, to avoid such an error.
1314
1315 Args:
1316 request: HttpRequest, Request to add to the batch.
1317 callback: callable, A callback to be called for this response, of the
1318 form callback(id, response, exception). The first parameter is the
1319 request id, and the second is the deserialized response object. The
1320 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1321 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001322 request_id: string, A unique id for the request. The id will be passed
1323 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001324
1325 Returns:
1326 None
1327
1328 Raises:
1329 BatchError if a media request is added to a batch.
1330 KeyError is the request_id is not unique.
1331 """
Xinan Line2dccec2018-12-07 05:28:33 +09001332
1333 if len(self._order) >= MAX_BATCH_LIMIT:
smstonea04b3c52018-12-10 14:13:41 -05001334 raise BatchError("Exceeded the maximum calls(%d) in a single batch request."
Xinan Line2dccec2018-12-07 05:28:33 +09001335 % MAX_BATCH_LIMIT)
John Asmuth864311d2014-04-24 15:46:08 -04001336 if request_id is None:
1337 request_id = self._new_id()
1338 if request.resumable is not None:
1339 raise BatchError("Media requests cannot be used in a batch request.")
1340 if request_id in self._requests:
1341 raise KeyError("A request with this ID already exists: %s" % request_id)
1342 self._requests[request_id] = request
1343 self._callbacks[request_id] = callback
1344 self._order.append(request_id)
1345
1346 def _execute(self, http, order, requests):
1347 """Serialize batch request, send to server, process response.
1348
1349 Args:
1350 http: httplib2.Http, an http object to be used to make the request with.
1351 order: list, list of request ids in the order they were added to the
1352 batch.
1353 request: list, list of request objects to send.
1354
1355 Raises:
1356 httplib2.HttpLib2Error if a transport error has occured.
1357 googleapiclient.errors.BatchError if the response is the wrong format.
1358 """
1359 message = MIMEMultipart('mixed')
1360 # Message should not write out it's own headers.
1361 setattr(message, '_write_headers', lambda self: None)
1362
1363 # Add all the individual requests.
1364 for request_id in order:
1365 request = requests[request_id]
1366
1367 msg = MIMENonMultipart('application', 'http')
1368 msg['Content-Transfer-Encoding'] = 'binary'
1369 msg['Content-ID'] = self._id_to_header(request_id)
1370
1371 body = self._serialize_request(request)
1372 msg.set_payload(body)
1373 message.attach(msg)
1374
Craig Citro72389b72014-07-15 17:12:50 -07001375 # encode the body: note that we can't use `as_string`, because
1376 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001377 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001378 g = Generator(fp, mangle_from_=False)
1379 g.flatten(message, unixfrom=False)
1380 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001381
1382 headers = {}
1383 headers['content-type'] = ('multipart/mixed; '
1384 'boundary="%s"') % message.get_boundary()
1385
1386 resp, content = http.request(self._batch_uri, method='POST', body=body,
1387 headers=headers)
1388
1389 if resp.status >= 300:
1390 raise HttpError(resp, content, uri=self._batch_uri)
1391
John Asmuth864311d2014-04-24 15:46:08 -04001392 # Prepend with a content-type header so FeedParser can handle it.
1393 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001394 # PY3's FeedParser only accepts unicode. So we should decode content
1395 # here, and encode each payload again.
1396 if six.PY3:
1397 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001398 for_parser = header + content
1399
1400 parser = FeedParser()
1401 parser.feed(for_parser)
1402 mime_response = parser.close()
1403
1404 if not mime_response.is_multipart():
1405 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1406 content=content)
1407
1408 for part in mime_response.get_payload():
1409 request_id = self._header_to_id(part['Content-ID'])
1410 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001411 # We encode content here to emulate normal http response.
1412 if isinstance(content, six.text_type):
1413 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001414 self._responses[request_id] = (response, content)
1415
1416 @util.positional(1)
1417 def execute(self, http=None):
1418 """Execute all the requests as a single batched HTTP request.
1419
1420 Args:
1421 http: httplib2.Http, an http object to be used in place of the one the
1422 HttpRequest request object was constructed with. If one isn't supplied
1423 then use a http object from the requests in this batch.
1424
1425 Returns:
1426 None
1427
1428 Raises:
1429 httplib2.HttpLib2Error if a transport error has occured.
1430 googleapiclient.errors.BatchError if the response is the wrong format.
1431 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001432 # If we have no requests return
1433 if len(self._order) == 0:
1434 return None
John Asmuth864311d2014-04-24 15:46:08 -04001435
1436 # If http is not supplied use the first valid one given in the requests.
1437 if http is None:
1438 for request_id in self._order:
1439 request = self._requests[request_id]
1440 if request is not None:
1441 http = request.http
1442 break
1443
1444 if http is None:
1445 raise ValueError("Missing a valid http object.")
1446
Gabriel Garcia23174be2016-05-25 17:28:07 +02001447 # Special case for OAuth2Credentials-style objects which have not yet been
1448 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001449 creds = _auth.get_credentials_from_http(http)
1450 if creds is not None:
1451 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001452 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001453 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001454
John Asmuth864311d2014-04-24 15:46:08 -04001455 self._execute(http, self._order, self._requests)
1456
1457 # Loop over all the requests and check for 401s. For each 401 request the
1458 # credentials should be refreshed and then sent again in a separate batch.
1459 redo_requests = {}
1460 redo_order = []
1461
1462 for request_id in self._order:
1463 resp, content = self._responses[request_id]
1464 if resp['status'] == '401':
1465 redo_order.append(request_id)
1466 request = self._requests[request_id]
1467 self._refresh_and_apply_credentials(request, http)
1468 redo_requests[request_id] = request
1469
1470 if redo_requests:
1471 self._execute(http, redo_order, redo_requests)
1472
1473 # Now process all callbacks that are erroring, and raise an exception for
1474 # ones that return a non-2xx response? Or add extra parameter to callback
1475 # that contains an HttpError?
1476
1477 for request_id in self._order:
1478 resp, content = self._responses[request_id]
1479
1480 request = self._requests[request_id]
1481 callback = self._callbacks[request_id]
1482
1483 response = None
1484 exception = None
1485 try:
1486 if resp.status >= 300:
1487 raise HttpError(resp, content, uri=request.uri)
1488 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001489 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001490 exception = e
1491
1492 if callback is not None:
1493 callback(request_id, response, exception)
1494 if self._callback is not None:
1495 self._callback(request_id, response, exception)
1496
1497
1498class HttpRequestMock(object):
1499 """Mock of HttpRequest.
1500
1501 Do not construct directly, instead use RequestMockBuilder.
1502 """
1503
1504 def __init__(self, resp, content, postproc):
1505 """Constructor for HttpRequestMock
1506
1507 Args:
1508 resp: httplib2.Response, the response to emulate coming from the request
1509 content: string, the response body
1510 postproc: callable, the post processing function usually supplied by
1511 the model class. See model.JsonModel.response() as an example.
1512 """
1513 self.resp = resp
1514 self.content = content
1515 self.postproc = postproc
1516 if resp is None:
1517 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1518 if 'reason' in self.resp:
1519 self.resp.reason = self.resp['reason']
1520
1521 def execute(self, http=None):
1522 """Execute the request.
1523
1524 Same behavior as HttpRequest.execute(), but the response is
1525 mocked and not really from an HTTP request/response.
1526 """
1527 return self.postproc(self.resp, self.content)
1528
1529
1530class RequestMockBuilder(object):
1531 """A simple mock of HttpRequest
1532
1533 Pass in a dictionary to the constructor that maps request methodIds to
1534 tuples of (httplib2.Response, content, opt_expected_body) that should be
1535 returned when that method is called. None may also be passed in for the
1536 httplib2.Response, in which case a 200 OK response will be generated.
1537 If an opt_expected_body (str or dict) is provided, it will be compared to
1538 the body and UnexpectedBodyError will be raised on inequality.
1539
1540 Example:
1541 response = '{"data": {"id": "tag:google.c...'
1542 requestBuilder = RequestMockBuilder(
1543 {
1544 'plus.activities.get': (None, response),
1545 }
1546 )
1547 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1548
1549 Methods that you do not supply a response for will return a
1550 200 OK with an empty string as the response content or raise an excpetion
1551 if check_unexpected is set to True. The methodId is taken from the rpcName
1552 in the discovery document.
1553
1554 For more details see the project wiki.
1555 """
1556
1557 def __init__(self, responses, check_unexpected=False):
1558 """Constructor for RequestMockBuilder
1559
1560 The constructed object should be a callable object
1561 that can replace the class HttpResponse.
1562
1563 responses - A dictionary that maps methodIds into tuples
1564 of (httplib2.Response, content). The methodId
1565 comes from the 'rpcName' field in the discovery
1566 document.
1567 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1568 should be raised on unsupplied method.
1569 """
1570 self.responses = responses
1571 self.check_unexpected = check_unexpected
1572
1573 def __call__(self, http, postproc, uri, method='GET', body=None,
1574 headers=None, methodId=None, resumable=None):
1575 """Implements the callable interface that discovery.build() expects
1576 of requestBuilder, which is to build an object compatible with
1577 HttpRequest.execute(). See that method for the description of the
1578 parameters and the expected response.
1579 """
1580 if methodId in self.responses:
1581 response = self.responses[methodId]
1582 resp, content = response[:2]
1583 if len(response) > 2:
1584 # Test the body against the supplied expected_body.
1585 expected_body = response[2]
1586 if bool(expected_body) != bool(body):
1587 # Not expecting a body and provided one
1588 # or expecting a body and not provided one.
1589 raise UnexpectedBodyError(expected_body, body)
1590 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001591 expected_body = json.loads(expected_body)
1592 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001593 if body != expected_body:
1594 raise UnexpectedBodyError(expected_body, body)
1595 return HttpRequestMock(resp, content, postproc)
1596 elif self.check_unexpected:
1597 raise UnexpectedMethodError(methodId=methodId)
1598 else:
1599 model = JsonModel(False)
1600 return HttpRequestMock(None, '{}', model.response)
1601
1602
1603class HttpMock(object):
1604 """Mock of httplib2.Http"""
1605
1606 def __init__(self, filename=None, headers=None):
1607 """
1608 Args:
1609 filename: string, absolute filename to read response from
1610 headers: dict, header to return with response
1611 """
1612 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001613 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001614 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001615 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001616 self.data = f.read()
1617 f.close()
1618 else:
1619 self.data = None
1620 self.response_headers = headers
1621 self.headers = None
1622 self.uri = None
1623 self.method = None
1624 self.body = None
1625 self.headers = None
1626
1627
1628 def request(self, uri,
1629 method='GET',
1630 body=None,
1631 headers=None,
1632 redirections=1,
1633 connection_type=None):
1634 self.uri = uri
1635 self.method = method
1636 self.body = body
1637 self.headers = headers
1638 return httplib2.Response(self.response_headers), self.data
1639
1640
1641class HttpMockSequence(object):
1642 """Mock of httplib2.Http
1643
1644 Mocks a sequence of calls to request returning different responses for each
1645 call. Create an instance initialized with the desired response headers
1646 and content and then use as if an httplib2.Http instance.
1647
1648 http = HttpMockSequence([
1649 ({'status': '401'}, ''),
1650 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1651 ({'status': '200'}, 'echo_request_headers'),
1652 ])
1653 resp, content = http.request("http://examples.com")
1654
1655 There are special values you can pass in for content to trigger
1656 behavours that are helpful in testing.
1657
1658 'echo_request_headers' means return the request headers in the response body
1659 'echo_request_headers_as_json' means return the request headers in
1660 the response body
1661 'echo_request_body' means return the request body in the response body
1662 'echo_request_uri' means return the request uri in the response body
1663 """
1664
1665 def __init__(self, iterable):
1666 """
1667 Args:
1668 iterable: iterable, a sequence of pairs of (headers, body)
1669 """
1670 self._iterable = iterable
1671 self.follow_redirects = True
1672
1673 def request(self, uri,
1674 method='GET',
1675 body=None,
1676 headers=None,
1677 redirections=1,
1678 connection_type=None):
1679 resp, content = self._iterable.pop(0)
1680 if content == 'echo_request_headers':
1681 content = headers
1682 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001683 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001684 elif content == 'echo_request_body':
1685 if hasattr(body, 'read'):
1686 content = body.read()
1687 else:
1688 content = body
1689 elif content == 'echo_request_uri':
1690 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001691 if isinstance(content, six.text_type):
1692 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001693 return httplib2.Response(resp), content
1694
1695
1696def set_user_agent(http, user_agent):
1697 """Set the user-agent on every request.
1698
1699 Args:
1700 http - An instance of httplib2.Http
1701 or something that acts like it.
1702 user_agent: string, the value for the user-agent header.
1703
1704 Returns:
1705 A modified instance of http that was passed in.
1706
1707 Example:
1708
1709 h = httplib2.Http()
1710 h = set_user_agent(h, "my-app-name/6.0")
1711
1712 Most of the time the user-agent will be set doing auth, this is for the rare
1713 cases where you are accessing an unauthenticated endpoint.
1714 """
1715 request_orig = http.request
1716
1717 # The closure that will replace 'httplib2.Http.request'.
1718 def new_request(uri, method='GET', body=None, headers=None,
1719 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1720 connection_type=None):
1721 """Modify the request headers to add the user-agent."""
1722 if headers is None:
1723 headers = {}
1724 if 'user-agent' in headers:
1725 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1726 else:
1727 headers['user-agent'] = user_agent
Bu Sun Kim1d2e2402019-05-17 16:03:03 -07001728 resp, content = request_orig(uri, method=method, body=body, headers=headers,
1729 redirections=redirections, connection_type=connection_type)
John Asmuth864311d2014-04-24 15:46:08 -04001730 return resp, content
1731
1732 http.request = new_request
1733 return http
1734
1735
1736def tunnel_patch(http):
1737 """Tunnel PATCH requests over POST.
1738 Args:
1739 http - An instance of httplib2.Http
1740 or something that acts like it.
1741
1742 Returns:
1743 A modified instance of http that was passed in.
1744
1745 Example:
1746
1747 h = httplib2.Http()
1748 h = tunnel_patch(h, "my-app-name/6.0")
1749
1750 Useful if you are running on a platform that doesn't support PATCH.
1751 Apply this last if you are using OAuth 1.0, as changing the method
1752 will result in a different signature.
1753 """
1754 request_orig = http.request
1755
1756 # The closure that will replace 'httplib2.Http.request'.
1757 def new_request(uri, method='GET', body=None, headers=None,
1758 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1759 connection_type=None):
1760 """Modify the request headers to add the user-agent."""
1761 if headers is None:
1762 headers = {}
1763 if method == 'PATCH':
1764 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001765 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001766 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1767 headers['x-http-method-override'] = "PATCH"
1768 method = 'POST'
Bu Sun Kim1d2e2402019-05-17 16:03:03 -07001769 resp, content = request_orig(uri, method=method, body=body, headers=headers,
1770 redirections=redirections, connection_type=connection_type)
John Asmuth864311d2014-04-24 15:46:08 -04001771 return resp, content
1772
1773 http.request = new_request
1774 return http
Igor Maravić22435292017-01-19 22:28:22 +01001775
1776
1777def build_http():
1778 """Builds httplib2.Http object
1779
1780 Returns:
1781 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1782 To override default timeout call
1783
1784 socket.setdefaulttimeout(timeout_in_sec)
1785
1786 before interacting with this method.
1787 """
1788 if socket.getdefaulttimeout() is not None:
1789 http_timeout = socket.getdefaulttimeout()
1790 else:
1791 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1792 return httplib2.Http(timeout=http_timeout)