blob: a57f83d5590c2e30f99bbf956bc387a9106df93e [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
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100152 for retry_num in range(num_retries + 1):
153 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000154 # Sleep before retrying.
155 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800156 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000157 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
158 sleep_time, retry_num, num_retries, req_type, method, uri,
Christian Clauss9fdc2b22019-07-22 19:43:21 +0200159 resp.status if resp else Exception)
eesheeshc6425a02016-02-12 15:07:06 +0000160 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100161
162 try:
eesheeshc6425a02016-02-12 15:07:06 +0000163 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100164 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000165 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000166 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000167 exception = ssl_error
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700168 except socket.timeout as socket_timeout:
169 # It's important that this be before socket.error as it's a subclass
170 # socket.timeout has no errorcode
171 exception = socket_timeout
eesheeshc6425a02016-02-12 15:07:06 +0000172 except socket.error as socket_error:
173 # errno's contents differ by platform, so we have to match by name.
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700174 if socket.errno.errorcode.get(socket_error.errno) not in {
175 'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED'}:
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100176 raise
eesheeshc6425a02016-02-12 15:07:06 +0000177 exception = socket_error
Bashir Sadjadc35150f2018-06-25 11:46:09 -0400178 except httplib2.ServerNotFoundError as server_not_found_error:
179 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000180
181 if exception:
182 if retry_num == num_retries:
183 raise exception
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100184 else:
185 continue
eesheeshc6425a02016-02-12 15:07:06 +0000186
187 if not _should_retry_response(resp.status, content):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100188 break
189
190 return resp, content
191
192
John Asmuth864311d2014-04-24 15:46:08 -0400193class MediaUploadProgress(object):
194 """Status of a resumable upload."""
195
196 def __init__(self, resumable_progress, total_size):
197 """Constructor.
198
199 Args:
200 resumable_progress: int, bytes sent so far.
201 total_size: int, total bytes in complete upload, or None if the total
202 upload size isn't known ahead of time.
203 """
204 self.resumable_progress = resumable_progress
205 self.total_size = total_size
206
207 def progress(self):
208 """Percent of upload completed, as a float.
209
210 Returns:
211 the percentage complete as a float, returning 0.0 if the total size of
212 the upload is unknown.
213 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300214 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400215 return float(self.resumable_progress) / float(self.total_size)
216 else:
217 return 0.0
218
219
220class MediaDownloadProgress(object):
221 """Status of a resumable download."""
222
223 def __init__(self, resumable_progress, total_size):
224 """Constructor.
225
226 Args:
227 resumable_progress: int, bytes received so far.
228 total_size: int, total bytes in complete download.
229 """
230 self.resumable_progress = resumable_progress
231 self.total_size = total_size
232
233 def progress(self):
234 """Percent of download completed, as a float.
235
236 Returns:
237 the percentage complete as a float, returning 0.0 if the total size of
238 the download is unknown.
239 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300240 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400241 return float(self.resumable_progress) / float(self.total_size)
242 else:
243 return 0.0
244
245
246class MediaUpload(object):
247 """Describes a media object to upload.
248
249 Base class that defines the interface of MediaUpload subclasses.
250
251 Note that subclasses of MediaUpload may allow you to control the chunksize
252 when uploading a media object. It is important to keep the size of the chunk
253 as large as possible to keep the upload efficient. Other factors may influence
254 the size of the chunk you use, particularly if you are working in an
255 environment where individual HTTP requests may have a hardcoded time limit,
256 such as under certain classes of requests under Google App Engine.
257
258 Streams are io.Base compatible objects that support seek(). Some MediaUpload
259 subclasses support using streams directly to upload data. Support for
260 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
261 platform that stream will be used for uploading the media object. The support
262 for streaming is indicated by has_stream() returning True. The stream() method
263 should return an io.Base object that supports seek(). On platforms where the
264 underlying httplib module supports streaming, for example Python 2.6 and
265 later, the stream will be passed into the http library which will result in
266 less memory being used and possibly faster uploads.
267
268 If you need to upload media that can't be uploaded using any of the existing
269 MediaUpload sub-class then you can sub-class MediaUpload for your particular
270 needs.
271 """
272
273 def chunksize(self):
274 """Chunk size for resumable uploads.
275
276 Returns:
277 Chunk size in bytes.
278 """
279 raise NotImplementedError()
280
281 def mimetype(self):
282 """Mime type of the body.
283
284 Returns:
285 Mime type.
286 """
287 return 'application/octet-stream'
288
289 def size(self):
290 """Size of upload.
291
292 Returns:
293 Size of the body, or None of the size is unknown.
294 """
295 return None
296
297 def resumable(self):
298 """Whether this upload is resumable.
299
300 Returns:
301 True if resumable upload or False.
302 """
303 return False
304
305 def getbytes(self, begin, end):
306 """Get bytes from the media.
307
308 Args:
309 begin: int, offset from beginning of file.
310 length: int, number of bytes to read, starting at begin.
311
312 Returns:
313 A string of bytes read. May be shorter than length if EOF was reached
314 first.
315 """
316 raise NotImplementedError()
317
318 def has_stream(self):
319 """Does the underlying upload support a streaming interface.
320
321 Streaming means it is an io.IOBase subclass that supports seek, i.e.
322 seekable() returns True.
323
324 Returns:
325 True if the call to stream() will return an instance of a seekable io.Base
326 subclass.
327 """
328 return False
329
330 def stream(self):
331 """A stream interface to the data being uploaded.
332
333 Returns:
334 The returned value is an io.IOBase subclass that supports seek, i.e.
335 seekable() returns True.
336 """
337 raise NotImplementedError()
338
339 @util.positional(1)
340 def _to_json(self, strip=None):
341 """Utility function for creating a JSON representation of a MediaUpload.
342
343 Args:
344 strip: array, An array of names of members to not include in the JSON.
345
346 Returns:
347 string, a JSON representation of this instance, suitable to pass to
348 from_json().
349 """
350 t = type(self)
351 d = copy.copy(self.__dict__)
352 if strip is not None:
353 for member in strip:
354 del d[member]
355 d['_class'] = t.__name__
356 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700357 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400358
359 def to_json(self):
360 """Create a JSON representation of an instance of MediaUpload.
361
362 Returns:
363 string, a JSON representation of this instance, suitable to pass to
364 from_json().
365 """
366 return self._to_json()
367
368 @classmethod
369 def new_from_json(cls, s):
370 """Utility class method to instantiate a MediaUpload subclass from a JSON
371 representation produced by to_json().
372
373 Args:
374 s: string, JSON from to_json().
375
376 Returns:
377 An instance of the subclass of MediaUpload that was serialized with
378 to_json().
379 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700380 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400381 # Find and call the right classmethod from_json() to restore the object.
382 module = data['_module']
383 m = __import__(module, fromlist=module.split('.')[:-1])
384 kls = getattr(m, data['_class'])
385 from_json = getattr(kls, 'from_json')
386 return from_json(s)
387
388
389class MediaIoBaseUpload(MediaUpload):
390 """A MediaUpload for a io.Base objects.
391
392 Note that the Python file object is compatible with io.Base and can be used
393 with this class also.
394
Pat Ferateed9affd2015-03-03 16:03:15 -0800395 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400396 media = MediaIoBaseUpload(fh, mimetype='image/png',
397 chunksize=1024*1024, resumable=True)
398 farm.animals().insert(
399 id='cow',
400 name='cow.png',
401 media_body=media).execute()
402
403 Depending on the platform you are working on, you may pass -1 as the
404 chunksize, which indicates that the entire file should be uploaded in a single
405 request. If the underlying platform supports streams, such as Python 2.6 or
406 later, then this can be very efficient as it avoids multiple connections, and
407 also avoids loading the entire file into memory before sending it. Note that
408 Google App Engine has a 5MB limit on request size, so you should never set
409 your chunksize larger than 5MB, or to -1.
410 """
411
412 @util.positional(3)
413 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
414 resumable=False):
415 """Constructor.
416
417 Args:
418 fd: io.Base or file object, The source of the bytes to upload. MUST be
419 opened in blocking mode, do not use streams opened in non-blocking mode.
420 The given stream must be seekable, that is, it must be able to call
421 seek() on fd.
422 mimetype: string, Mime-type of the file.
423 chunksize: int, File will be uploaded in chunks of this many bytes. Only
424 used if resumable=True. Pass in a value of -1 if the file is to be
425 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
426 on request size, so you should never set your chunksize larger than 5MB,
427 or to -1.
428 resumable: bool, True if this is a resumable upload. False means upload
429 in a single request.
430 """
431 super(MediaIoBaseUpload, self).__init__()
432 self._fd = fd
433 self._mimetype = mimetype
434 if not (chunksize == -1 or chunksize > 0):
435 raise InvalidChunkSizeError()
436 self._chunksize = chunksize
437 self._resumable = resumable
438
439 self._fd.seek(0, os.SEEK_END)
440 self._size = self._fd.tell()
441
442 def chunksize(self):
443 """Chunk size for resumable uploads.
444
445 Returns:
446 Chunk size in bytes.
447 """
448 return self._chunksize
449
450 def mimetype(self):
451 """Mime type of the body.
452
453 Returns:
454 Mime type.
455 """
456 return self._mimetype
457
458 def size(self):
459 """Size of upload.
460
461 Returns:
462 Size of the body, or None of the size is unknown.
463 """
464 return self._size
465
466 def resumable(self):
467 """Whether this upload is resumable.
468
469 Returns:
470 True if resumable upload or False.
471 """
472 return self._resumable
473
474 def getbytes(self, begin, length):
475 """Get bytes from the media.
476
477 Args:
478 begin: int, offset from beginning of file.
479 length: int, number of bytes to read, starting at begin.
480
481 Returns:
482 A string of bytes read. May be shorted than length if EOF was reached
483 first.
484 """
485 self._fd.seek(begin)
486 return self._fd.read(length)
487
488 def has_stream(self):
489 """Does the underlying upload support a streaming interface.
490
491 Streaming means it is an io.IOBase subclass that supports seek, i.e.
492 seekable() returns True.
493
494 Returns:
495 True if the call to stream() will return an instance of a seekable io.Base
496 subclass.
497 """
498 return True
499
500 def stream(self):
501 """A stream interface to the data being uploaded.
502
503 Returns:
504 The returned value is an io.IOBase subclass that supports seek, i.e.
505 seekable() returns True.
506 """
507 return self._fd
508
509 def to_json(self):
510 """This upload type is not serializable."""
511 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
512
513
514class MediaFileUpload(MediaIoBaseUpload):
515 """A MediaUpload for a file.
516
517 Construct a MediaFileUpload and pass as the media_body parameter of the
518 method. For example, if we had a service that allowed uploading images:
519
John Asmuth864311d2014-04-24 15:46:08 -0400520 media = MediaFileUpload('cow.png', mimetype='image/png',
521 chunksize=1024*1024, resumable=True)
522 farm.animals().insert(
523 id='cow',
524 name='cow.png',
525 media_body=media).execute()
526
527 Depending on the platform you are working on, you may pass -1 as the
528 chunksize, which indicates that the entire file should be uploaded in a single
529 request. If the underlying platform supports streams, such as Python 2.6 or
530 later, then this can be very efficient as it avoids multiple connections, and
531 also avoids loading the entire file into memory before sending it. Note that
532 Google App Engine has a 5MB limit on request size, so you should never set
533 your chunksize larger than 5MB, or to -1.
534 """
535
536 @util.positional(2)
537 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
538 resumable=False):
539 """Constructor.
540
541 Args:
542 filename: string, Name of the file.
543 mimetype: string, Mime-type of the file. If None then a mime-type will be
544 guessed from the file extension.
545 chunksize: int, File will be uploaded in chunks of this many bytes. Only
546 used if resumable=True. Pass in a value of -1 if the file is to be
547 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
548 on request size, so you should never set your chunksize larger than 5MB,
549 or to -1.
550 resumable: bool, True if this is a resumable upload. False means upload
551 in a single request.
552 """
553 self._filename = filename
554 fd = open(self._filename, 'rb')
555 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800556 # No mimetype provided, make a guess.
557 mimetype, _ = mimetypes.guess_type(filename)
558 if mimetype is None:
559 # Guess failed, use octet-stream.
560 mimetype = 'application/octet-stream'
Xiaofei Wang20b67582019-07-17 11:16:53 -0700561 super(MediaFileUpload, self).__init__(fd, mimetype,
562 chunksize=chunksize,
John Asmuth864311d2014-04-24 15:46:08 -0400563 resumable=resumable)
564
Xiaofei Wang20b67582019-07-17 11:16:53 -0700565 def __del__(self):
566 self._fd.close()
567
John Asmuth864311d2014-04-24 15:46:08 -0400568 def to_json(self):
569 """Creating a JSON representation of an instance of MediaFileUpload.
570
571 Returns:
572 string, a JSON representation of this instance, suitable to pass to
573 from_json().
574 """
575 return self._to_json(strip=['_fd'])
576
577 @staticmethod
578 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700579 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400580 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
581 chunksize=d['_chunksize'], resumable=d['_resumable'])
582
583
584class MediaInMemoryUpload(MediaIoBaseUpload):
585 """MediaUpload for a chunk of bytes.
586
587 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
588 the stream.
589 """
590
591 @util.positional(2)
592 def __init__(self, body, mimetype='application/octet-stream',
593 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
594 """Create a new MediaInMemoryUpload.
595
596 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
597 the stream.
598
599 Args:
600 body: string, Bytes of body content.
601 mimetype: string, Mime-type of the file or default of
602 'application/octet-stream'.
603 chunksize: int, File will be uploaded in chunks of this many bytes. Only
604 used if resumable=True.
605 resumable: bool, True if this is a resumable upload. False means upload
606 in a single request.
607 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800608 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400609 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
610 resumable=resumable)
611
612
613class MediaIoBaseDownload(object):
614 """"Download media resources.
615
616 Note that the Python file object is compatible with io.Base and can be used
617 with this class also.
618
619
620 Example:
621 request = farms.animals().get_media(id='cow')
622 fh = io.FileIO('cow.png', mode='wb')
623 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
624
625 done = False
626 while done is False:
627 status, done = downloader.next_chunk()
628 if status:
629 print "Download %d%%." % int(status.progress() * 100)
630 print "Download Complete!"
631 """
632
633 @util.positional(3)
634 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
635 """Constructor.
636
637 Args:
638 fd: io.Base or file object, The stream in which to write the downloaded
639 bytes.
640 request: googleapiclient.http.HttpRequest, the media request to perform in
641 chunks.
642 chunksize: int, File will be downloaded in chunks of this many bytes.
643 """
644 self._fd = fd
645 self._request = request
646 self._uri = request.uri
647 self._chunksize = chunksize
648 self._progress = 0
649 self._total_size = None
650 self._done = False
651
652 # Stubs for testing.
653 self._sleep = time.sleep
654 self._rand = random.random
655
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400656 self._headers = {}
657 for k, v in six.iteritems(request.headers):
658 # allow users to supply custom headers by setting them on the request
659 # but strip out the ones that are set by default on requests generated by
660 # API methods like Drive's files().get(fileId=...)
661 if not k.lower() in ('accept', 'accept-encoding', 'user-agent'):
662 self._headers[k] = v
663
John Asmuth864311d2014-04-24 15:46:08 -0400664 @util.positional(1)
665 def next_chunk(self, num_retries=0):
666 """Get the next chunk of the download.
667
668 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500669 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400670 exponential backoff. If all retries fail, the raised HttpError
671 represents the last request. If zero (default), we attempt the
672 request only once.
673
674 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700675 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400676 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100677 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400678
679 Raises:
680 googleapiclient.errors.HttpError if the response was not a 2xx.
681 httplib2.HttpLib2Error if a transport error has occured.
682 """
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400683 headers = self._headers.copy()
684 headers['range'] = 'bytes=%d-%d' % (
John Asmuth864311d2014-04-24 15:46:08 -0400685 self._progress, self._progress + self._chunksize)
John Asmuth864311d2014-04-24 15:46:08 -0400686 http = self._request.http
687
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100688 resp, content = _retry_request(
689 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
690 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400691
692 if resp.status in [200, 206]:
693 if 'content-location' in resp and resp['content-location'] != self._uri:
694 self._uri = resp['content-location']
695 self._progress += len(content)
696 self._fd.write(content)
697
698 if 'content-range' in resp:
699 content_range = resp['content-range']
700 length = content_range.rsplit('/', 1)[1]
701 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800702 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800703 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400704
Daniel44067782018-01-16 23:17:56 +0100705 if self._total_size is None or self._progress == self._total_size:
John Asmuth864311d2014-04-24 15:46:08 -0400706 self._done = True
707 return MediaDownloadProgress(self._progress, self._total_size), self._done
708 else:
709 raise HttpError(resp, content, uri=self._uri)
710
711
712class _StreamSlice(object):
713 """Truncated stream.
714
715 Takes a stream and presents a stream that is a slice of the original stream.
716 This is used when uploading media in chunks. In later versions of Python a
717 stream can be passed to httplib in place of the string of data to send. The
718 problem is that httplib just blindly reads to the end of the stream. This
719 wrapper presents a virtual stream that only reads to the end of the chunk.
720 """
721
722 def __init__(self, stream, begin, chunksize):
723 """Constructor.
724
725 Args:
726 stream: (io.Base, file object), the stream to wrap.
727 begin: int, the seek position the chunk begins at.
728 chunksize: int, the size of the chunk.
729 """
730 self._stream = stream
731 self._begin = begin
732 self._chunksize = chunksize
733 self._stream.seek(begin)
734
735 def read(self, n=-1):
736 """Read n bytes.
737
738 Args:
739 n, int, the number of bytes to read.
740
741 Returns:
742 A string of length 'n', or less if EOF is reached.
743 """
744 # The data left available to read sits in [cur, end)
745 cur = self._stream.tell()
746 end = self._begin + self._chunksize
747 if n == -1 or cur + n > end:
748 n = end - cur
749 return self._stream.read(n)
750
751
752class HttpRequest(object):
753 """Encapsulates a single HTTP request."""
754
755 @util.positional(4)
756 def __init__(self, http, postproc, uri,
757 method='GET',
758 body=None,
759 headers=None,
760 methodId=None,
761 resumable=None):
762 """Constructor for an HttpRequest.
763
764 Args:
765 http: httplib2.Http, the transport object to use to make a request
766 postproc: callable, called on the HTTP response and content to transform
767 it into a data object before returning, or raising an exception
768 on an error.
769 uri: string, the absolute URI to send the request to
770 method: string, the HTTP method to use
771 body: string, the request body of the HTTP request,
772 headers: dict, the HTTP request headers
773 methodId: string, a unique identifier for the API method being called.
774 resumable: MediaUpload, None if this is not a resumbale request.
775 """
776 self.uri = uri
777 self.method = method
778 self.body = body
779 self.headers = headers or {}
780 self.methodId = methodId
781 self.http = http
782 self.postproc = postproc
783 self.resumable = resumable
784 self.response_callbacks = []
785 self._in_error_state = False
786
John Asmuth864311d2014-04-24 15:46:08 -0400787 # The size of the non-media part of the request.
788 self.body_size = len(self.body or '')
789
790 # The resumable URI to send chunks to.
791 self.resumable_uri = None
792
793 # The bytes that have been uploaded.
794 self.resumable_progress = 0
795
796 # Stubs for testing.
797 self._rand = random.random
798 self._sleep = time.sleep
799
800 @util.positional(1)
801 def execute(self, http=None, num_retries=0):
802 """Execute the request.
803
804 Args:
805 http: httplib2.Http, an http object to be used in place of the
806 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500807 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400808 exponential backoff. If all retries fail, the raised HttpError
809 represents the last request. If zero (default), we attempt the
810 request only once.
811
812 Returns:
813 A deserialized object model of the response body as determined
814 by the postproc.
815
816 Raises:
817 googleapiclient.errors.HttpError if the response was not a 2xx.
818 httplib2.HttpLib2Error if a transport error has occured.
819 """
820 if http is None:
821 http = self.http
822
823 if self.resumable:
824 body = None
825 while body is None:
826 _, body = self.next_chunk(http=http, num_retries=num_retries)
827 return body
828
829 # Non-resumable case.
830
831 if 'content-length' not in self.headers:
832 self.headers['content-length'] = str(self.body_size)
833 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800834 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400835 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
836 self.method = 'POST'
837 self.headers['x-http-method-override'] = 'GET'
838 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800839 parsed = urlparse(self.uri)
840 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400841 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
842 None)
843 )
844 self.body = parsed.query
845 self.headers['content-length'] = str(len(self.body))
846
847 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100848 resp, content = _retry_request(
849 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
850 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400851
852 for callback in self.response_callbacks:
853 callback(resp)
854 if resp.status >= 300:
855 raise HttpError(resp, content, uri=self.uri)
856 return self.postproc(resp, content)
857
858 @util.positional(2)
859 def add_response_callback(self, cb):
860 """add_response_headers_callback
861
862 Args:
863 cb: Callback to be called on receiving the response headers, of signature:
864
865 def cb(resp):
866 # Where resp is an instance of httplib2.Response
867 """
868 self.response_callbacks.append(cb)
869
870 @util.positional(1)
871 def next_chunk(self, http=None, num_retries=0):
872 """Execute the next step of a resumable upload.
873
874 Can only be used if the method being executed supports media uploads and
875 the MediaUpload object passed in was flagged as using resumable upload.
876
877 Example:
878
879 media = MediaFileUpload('cow.png', mimetype='image/png',
880 chunksize=1000, resumable=True)
881 request = farm.animals().insert(
882 id='cow',
883 name='cow.png',
884 media_body=media)
885
886 response = None
887 while response is None:
888 status, response = request.next_chunk()
889 if status:
890 print "Upload %d%% complete." % int(status.progress() * 100)
891
892
893 Args:
894 http: httplib2.Http, an http object to be used in place of the
895 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500896 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400897 exponential backoff. If all retries fail, the raised HttpError
898 represents the last request. If zero (default), we attempt the
899 request only once.
900
901 Returns:
902 (status, body): (ResumableMediaStatus, object)
903 The body will be None until the resumable media is fully uploaded.
904
905 Raises:
906 googleapiclient.errors.HttpError if the response was not a 2xx.
907 httplib2.HttpLib2Error if a transport error has occured.
908 """
909 if http is None:
910 http = self.http
911
912 if self.resumable.size() is None:
913 size = '*'
914 else:
915 size = str(self.resumable.size())
916
917 if self.resumable_uri is None:
918 start_headers = copy.copy(self.headers)
919 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
920 if size != '*':
921 start_headers['X-Upload-Content-Length'] = size
922 start_headers['content-length'] = str(self.body_size)
923
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100924 resp, content = _retry_request(
925 http, num_retries, 'resumable URI request', self._sleep, self._rand,
926 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400927
928 if resp.status == 200 and 'location' in resp:
929 self.resumable_uri = resp['location']
930 else:
931 raise ResumableUploadError(resp, content)
932 elif self._in_error_state:
933 # If we are in an error state then query the server for current state of
934 # the upload by sending an empty PUT and reading the 'range' header in
935 # the response.
936 headers = {
937 'Content-Range': 'bytes */%s' % size,
938 'content-length': '0'
939 }
940 resp, content = http.request(self.resumable_uri, 'PUT',
941 headers=headers)
942 status, body = self._process_response(resp, content)
943 if body:
944 # The upload was complete.
945 return (status, body)
946
e00Efafe8582015-10-10 18:19:37 +0200947 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400948 data = self.resumable.stream()
949 if self.resumable.chunksize() == -1:
950 data.seek(self.resumable_progress)
951 chunk_end = self.resumable.size() - self.resumable_progress - 1
952 else:
953 # Doing chunking with a stream, so wrap a slice of the stream.
954 data = _StreamSlice(data, self.resumable_progress,
955 self.resumable.chunksize())
956 chunk_end = min(
957 self.resumable_progress + self.resumable.chunksize() - 1,
958 self.resumable.size() - 1)
959 else:
960 data = self.resumable.getbytes(
961 self.resumable_progress, self.resumable.chunksize())
962
963 # A short read implies that we are at EOF, so finish the upload.
964 if len(data) < self.resumable.chunksize():
965 size = str(self.resumable_progress + len(data))
966
967 chunk_end = self.resumable_progress + len(data) - 1
968
969 headers = {
970 'Content-Range': 'bytes %d-%d/%s' % (
971 self.resumable_progress, chunk_end, size),
972 # Must set the content-length header here because httplib can't
973 # calculate the size when working with _StreamSlice.
974 'Content-Length': str(chunk_end - self.resumable_progress + 1)
975 }
976
INADA Naokie4ea1a92015-03-04 03:45:42 +0900977 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400978 if retry_num > 0:
979 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800980 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400981 'Retry #%d for media upload: %s %s, following status: %d'
982 % (retry_num, self.method, self.uri, resp.status))
983
984 try:
985 resp, content = http.request(self.resumable_uri, method='PUT',
986 body=data,
987 headers=headers)
988 except:
989 self._in_error_state = True
990 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500991 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400992 break
993
994 return self._process_response(resp, content)
995
996 def _process_response(self, resp, content):
997 """Process the response from a single chunk upload.
998
999 Args:
1000 resp: httplib2.Response, the response object.
1001 content: string, the content of the response.
1002
1003 Returns:
1004 (status, body): (ResumableMediaStatus, object)
1005 The body will be None until the resumable media is fully uploaded.
1006
1007 Raises:
1008 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1009 """
1010 if resp.status in [200, 201]:
1011 self._in_error_state = False
1012 return None, self.postproc(resp, content)
1013 elif resp.status == 308:
1014 self._in_error_state = False
1015 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001016 try:
1017 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1018 except KeyError:
1019 # If resp doesn't contain range header, resumable progress is 0
1020 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001021 if 'location' in resp:
1022 self.resumable_uri = resp['location']
1023 else:
1024 self._in_error_state = True
1025 raise HttpError(resp, content, uri=self.uri)
1026
1027 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1028 None)
1029
1030 def to_json(self):
1031 """Returns a JSON representation of the HttpRequest."""
1032 d = copy.copy(self.__dict__)
1033 if d['resumable'] is not None:
1034 d['resumable'] = self.resumable.to_json()
1035 del d['http']
1036 del d['postproc']
1037 del d['_sleep']
1038 del d['_rand']
1039
Craig Citro6ae34d72014-08-18 23:10:09 -07001040 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001041
1042 @staticmethod
1043 def from_json(s, http, postproc):
1044 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001045 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001046 if d['resumable'] is not None:
1047 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1048 return HttpRequest(
1049 http,
1050 postproc,
1051 uri=d['uri'],
1052 method=d['method'],
1053 body=d['body'],
1054 headers=d['headers'],
1055 methodId=d['methodId'],
1056 resumable=d['resumable'])
1057
1058
1059class BatchHttpRequest(object):
1060 """Batches multiple HttpRequest objects into a single HTTP request.
1061
1062 Example:
1063 from googleapiclient.http import BatchHttpRequest
1064
1065 def list_animals(request_id, response, exception):
1066 \"\"\"Do something with the animals list response.\"\"\"
1067 if exception is not None:
1068 # Do something with the exception.
1069 pass
1070 else:
1071 # Do something with the response.
1072 pass
1073
1074 def list_farmers(request_id, response, exception):
1075 \"\"\"Do something with the farmers list response.\"\"\"
1076 if exception is not None:
1077 # Do something with the exception.
1078 pass
1079 else:
1080 # Do something with the response.
1081 pass
1082
1083 service = build('farm', 'v2')
1084
1085 batch = BatchHttpRequest()
1086
1087 batch.add(service.animals().list(), list_animals)
1088 batch.add(service.farmers().list(), list_farmers)
1089 batch.execute(http=http)
1090 """
1091
1092 @util.positional(1)
1093 def __init__(self, callback=None, batch_uri=None):
1094 """Constructor for a BatchHttpRequest.
1095
1096 Args:
1097 callback: callable, A callback to be called for each response, of the
1098 form callback(id, response, exception). The first parameter is the
1099 request id, and the second is the deserialized response object. The
1100 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1101 occurred while processing the request, or None if no error occurred.
1102 batch_uri: string, URI to send batch requests to.
1103 """
1104 if batch_uri is None:
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001105 batch_uri = _LEGACY_BATCH_URI
1106
1107 if batch_uri == _LEGACY_BATCH_URI:
1108 LOGGER.warn(
1109 "You have constructed a BatchHttpRequest using the legacy batch "
1110 "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1111 "Please provide the API-specific endpoint or use "
1112 "service.new_batch_http_request(). For more details see "
1113 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1114 "and https://developers.google.com/api-client-library/python/guide/batch.",
1115 _LEGACY_BATCH_URI)
John Asmuth864311d2014-04-24 15:46:08 -04001116 self._batch_uri = batch_uri
1117
1118 # Global callback to be called for each individual response in the batch.
1119 self._callback = callback
1120
1121 # A map from id to request.
1122 self._requests = {}
1123
1124 # A map from id to callback.
1125 self._callbacks = {}
1126
1127 # List of request ids, in the order in which they were added.
1128 self._order = []
1129
1130 # The last auto generated id.
1131 self._last_auto_id = 0
1132
1133 # Unique ID on which to base the Content-ID headers.
1134 self._base_id = None
1135
1136 # A map from request id to (httplib2.Response, content) response pairs
1137 self._responses = {}
1138
1139 # A map of id(Credentials) that have been refreshed.
1140 self._refreshed_credentials = {}
1141
1142 def _refresh_and_apply_credentials(self, request, http):
1143 """Refresh the credentials and apply to the request.
1144
1145 Args:
1146 request: HttpRequest, the request.
1147 http: httplib2.Http, the global http object for the batch.
1148 """
1149 # For the credentials to refresh, but only once per refresh_token
1150 # If there is no http per the request then refresh the http passed in
1151 # via execute()
1152 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001153 request_credentials = False
1154
1155 if request.http is not None:
1156 creds = _auth.get_credentials_from_http(request.http)
1157 request_credentials = True
1158
1159 if creds is None and http is not None:
1160 creds = _auth.get_credentials_from_http(http)
1161
John Asmuth864311d2014-04-24 15:46:08 -04001162 if creds is not None:
1163 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001164 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001165 self._refreshed_credentials[id(creds)] = 1
1166
1167 # Only apply the credentials if we are using the http object passed in,
1168 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001169 if request.http is None or not request_credentials:
1170 _auth.apply_credentials(creds, request.headers)
1171
John Asmuth864311d2014-04-24 15:46:08 -04001172
1173 def _id_to_header(self, id_):
1174 """Convert an id to a Content-ID header value.
1175
1176 Args:
1177 id_: string, identifier of individual request.
1178
1179 Returns:
1180 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1181 the value because Content-ID headers are supposed to be universally
1182 unique.
1183 """
1184 if self._base_id is None:
1185 self._base_id = uuid.uuid4()
1186
Chris McDonough3cf5e602018-07-18 16:18:38 -04001187 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1188 # line folding works properly on Python 3; see
1189 # https://github.com/google/google-api-python-client/issues/164
1190 return '<%s + %s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001191
1192 def _header_to_id(self, header):
1193 """Convert a Content-ID header value to an id.
1194
1195 Presumes the Content-ID header conforms to the format that _id_to_header()
1196 returns.
1197
1198 Args:
1199 header: string, Content-ID header value.
1200
1201 Returns:
1202 The extracted id value.
1203
1204 Raises:
1205 BatchError if the header is not in the expected format.
1206 """
1207 if header[0] != '<' or header[-1] != '>':
1208 raise BatchError("Invalid value for Content-ID: %s" % header)
1209 if '+' not in header:
1210 raise BatchError("Invalid value for Content-ID: %s" % header)
Chris McDonough3cf5e602018-07-18 16:18:38 -04001211 base, id_ = header[1:-1].split(' + ', 1)
John Asmuth864311d2014-04-24 15:46:08 -04001212
Pat Ferated5b61bd2015-03-03 16:04:11 -08001213 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001214
1215 def _serialize_request(self, request):
1216 """Convert an HttpRequest object into a string.
1217
1218 Args:
1219 request: HttpRequest, the request to serialize.
1220
1221 Returns:
1222 The request as a string in application/http format.
1223 """
1224 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001225 parsed = urlparse(request.uri)
1226 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001227 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001228 )
1229 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1230 major, minor = request.headers.get('content-type', 'application/json').split('/')
1231 msg = MIMENonMultipart(major, minor)
1232 headers = request.headers.copy()
1233
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001234 if request.http is not None:
1235 credentials = _auth.get_credentials_from_http(request.http)
1236 if credentials is not None:
1237 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001238
1239 # MIMENonMultipart adds its own Content-Type header.
1240 if 'content-type' in headers:
1241 del headers['content-type']
1242
INADA Naokie4ea1a92015-03-04 03:45:42 +09001243 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001244 msg[key] = value
1245 msg['Host'] = parsed.netloc
1246 msg.set_unixfrom(None)
1247
1248 if request.body is not None:
1249 msg.set_payload(request.body)
1250 msg['content-length'] = str(len(request.body))
1251
1252 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001253 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001254 # maxheaderlen=0 means don't line wrap headers.
1255 g = Generator(fp, maxheaderlen=0)
1256 g.flatten(msg, unixfrom=False)
1257 body = fp.getvalue()
1258
Pat Feratec9abbbd2015-03-03 18:00:38 -08001259 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001260
1261 def _deserialize_response(self, payload):
1262 """Convert string into httplib2 response and content.
1263
1264 Args:
1265 payload: string, headers and body as a string.
1266
1267 Returns:
1268 A pair (resp, content), such as would be returned from httplib2.request.
1269 """
1270 # Strip off the status line
1271 status_line, payload = payload.split('\n', 1)
1272 protocol, status, reason = status_line.split(' ', 2)
1273
1274 # Parse the rest of the response
1275 parser = FeedParser()
1276 parser.feed(payload)
1277 msg = parser.close()
1278 msg['status'] = status
1279
1280 # Create httplib2.Response from the parsed headers.
1281 resp = httplib2.Response(msg)
1282 resp.reason = reason
1283 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1284
1285 content = payload.split('\r\n\r\n', 1)[1]
1286
1287 return resp, content
1288
1289 def _new_id(self):
1290 """Create a new id.
1291
1292 Auto incrementing number that avoids conflicts with ids already used.
1293
1294 Returns:
1295 string, a new unique id.
1296 """
1297 self._last_auto_id += 1
1298 while str(self._last_auto_id) in self._requests:
1299 self._last_auto_id += 1
1300 return str(self._last_auto_id)
1301
1302 @util.positional(2)
1303 def add(self, request, callback=None, request_id=None):
1304 """Add a new request.
1305
1306 Every callback added will be paired with a unique id, the request_id. That
1307 unique id will be passed back to the callback when the response comes back
1308 from the server. The default behavior is to have the library generate it's
1309 own unique id. If the caller passes in a request_id then they must ensure
1310 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001311 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001312 request id, to avoid such an error.
1313
1314 Args:
1315 request: HttpRequest, Request to add to the batch.
1316 callback: callable, A callback to be called for this response, of the
1317 form callback(id, response, exception). The first parameter is the
1318 request id, and the second is the deserialized response object. The
1319 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1320 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001321 request_id: string, A unique id for the request. The id will be passed
1322 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001323
1324 Returns:
1325 None
1326
1327 Raises:
1328 BatchError if a media request is added to a batch.
1329 KeyError is the request_id is not unique.
1330 """
Xinan Line2dccec2018-12-07 05:28:33 +09001331
1332 if len(self._order) >= MAX_BATCH_LIMIT:
smstonea04b3c52018-12-10 14:13:41 -05001333 raise BatchError("Exceeded the maximum calls(%d) in a single batch request."
Xinan Line2dccec2018-12-07 05:28:33 +09001334 % MAX_BATCH_LIMIT)
John Asmuth864311d2014-04-24 15:46:08 -04001335 if request_id is None:
1336 request_id = self._new_id()
1337 if request.resumable is not None:
1338 raise BatchError("Media requests cannot be used in a batch request.")
1339 if request_id in self._requests:
1340 raise KeyError("A request with this ID already exists: %s" % request_id)
1341 self._requests[request_id] = request
1342 self._callbacks[request_id] = callback
1343 self._order.append(request_id)
1344
1345 def _execute(self, http, order, requests):
1346 """Serialize batch request, send to server, process response.
1347
1348 Args:
1349 http: httplib2.Http, an http object to be used to make the request with.
1350 order: list, list of request ids in the order they were added to the
1351 batch.
1352 request: list, list of request objects to send.
1353
1354 Raises:
1355 httplib2.HttpLib2Error if a transport error has occured.
1356 googleapiclient.errors.BatchError if the response is the wrong format.
1357 """
1358 message = MIMEMultipart('mixed')
1359 # Message should not write out it's own headers.
1360 setattr(message, '_write_headers', lambda self: None)
1361
1362 # Add all the individual requests.
1363 for request_id in order:
1364 request = requests[request_id]
1365
1366 msg = MIMENonMultipart('application', 'http')
1367 msg['Content-Transfer-Encoding'] = 'binary'
1368 msg['Content-ID'] = self._id_to_header(request_id)
1369
1370 body = self._serialize_request(request)
1371 msg.set_payload(body)
1372 message.attach(msg)
1373
Craig Citro72389b72014-07-15 17:12:50 -07001374 # encode the body: note that we can't use `as_string`, because
1375 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001376 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001377 g = Generator(fp, mangle_from_=False)
1378 g.flatten(message, unixfrom=False)
1379 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001380
1381 headers = {}
1382 headers['content-type'] = ('multipart/mixed; '
1383 'boundary="%s"') % message.get_boundary()
1384
1385 resp, content = http.request(self._batch_uri, method='POST', body=body,
1386 headers=headers)
1387
1388 if resp.status >= 300:
1389 raise HttpError(resp, content, uri=self._batch_uri)
1390
John Asmuth864311d2014-04-24 15:46:08 -04001391 # Prepend with a content-type header so FeedParser can handle it.
1392 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001393 # PY3's FeedParser only accepts unicode. So we should decode content
1394 # here, and encode each payload again.
1395 if six.PY3:
1396 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001397 for_parser = header + content
1398
1399 parser = FeedParser()
1400 parser.feed(for_parser)
1401 mime_response = parser.close()
1402
1403 if not mime_response.is_multipart():
1404 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1405 content=content)
1406
1407 for part in mime_response.get_payload():
1408 request_id = self._header_to_id(part['Content-ID'])
1409 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001410 # We encode content here to emulate normal http response.
1411 if isinstance(content, six.text_type):
1412 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001413 self._responses[request_id] = (response, content)
1414
1415 @util.positional(1)
1416 def execute(self, http=None):
1417 """Execute all the requests as a single batched HTTP request.
1418
1419 Args:
1420 http: httplib2.Http, an http object to be used in place of the one the
1421 HttpRequest request object was constructed with. If one isn't supplied
1422 then use a http object from the requests in this batch.
1423
1424 Returns:
1425 None
1426
1427 Raises:
1428 httplib2.HttpLib2Error if a transport error has occured.
1429 googleapiclient.errors.BatchError if the response is the wrong format.
1430 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001431 # If we have no requests return
1432 if len(self._order) == 0:
1433 return None
John Asmuth864311d2014-04-24 15:46:08 -04001434
1435 # If http is not supplied use the first valid one given in the requests.
1436 if http is None:
1437 for request_id in self._order:
1438 request = self._requests[request_id]
1439 if request is not None:
1440 http = request.http
1441 break
1442
1443 if http is None:
1444 raise ValueError("Missing a valid http object.")
1445
Gabriel Garcia23174be2016-05-25 17:28:07 +02001446 # Special case for OAuth2Credentials-style objects which have not yet been
1447 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001448 creds = _auth.get_credentials_from_http(http)
1449 if creds is not None:
1450 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001451 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001452 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001453
John Asmuth864311d2014-04-24 15:46:08 -04001454 self._execute(http, self._order, self._requests)
1455
1456 # Loop over all the requests and check for 401s. For each 401 request the
1457 # credentials should be refreshed and then sent again in a separate batch.
1458 redo_requests = {}
1459 redo_order = []
1460
1461 for request_id in self._order:
1462 resp, content = self._responses[request_id]
1463 if resp['status'] == '401':
1464 redo_order.append(request_id)
1465 request = self._requests[request_id]
1466 self._refresh_and_apply_credentials(request, http)
1467 redo_requests[request_id] = request
1468
1469 if redo_requests:
1470 self._execute(http, redo_order, redo_requests)
1471
1472 # Now process all callbacks that are erroring, and raise an exception for
1473 # ones that return a non-2xx response? Or add extra parameter to callback
1474 # that contains an HttpError?
1475
1476 for request_id in self._order:
1477 resp, content = self._responses[request_id]
1478
1479 request = self._requests[request_id]
1480 callback = self._callbacks[request_id]
1481
1482 response = None
1483 exception = None
1484 try:
1485 if resp.status >= 300:
1486 raise HttpError(resp, content, uri=request.uri)
1487 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001488 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001489 exception = e
1490
1491 if callback is not None:
1492 callback(request_id, response, exception)
1493 if self._callback is not None:
1494 self._callback(request_id, response, exception)
1495
1496
1497class HttpRequestMock(object):
1498 """Mock of HttpRequest.
1499
1500 Do not construct directly, instead use RequestMockBuilder.
1501 """
1502
1503 def __init__(self, resp, content, postproc):
1504 """Constructor for HttpRequestMock
1505
1506 Args:
1507 resp: httplib2.Response, the response to emulate coming from the request
1508 content: string, the response body
1509 postproc: callable, the post processing function usually supplied by
1510 the model class. See model.JsonModel.response() as an example.
1511 """
1512 self.resp = resp
1513 self.content = content
1514 self.postproc = postproc
1515 if resp is None:
1516 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1517 if 'reason' in self.resp:
1518 self.resp.reason = self.resp['reason']
1519
1520 def execute(self, http=None):
1521 """Execute the request.
1522
1523 Same behavior as HttpRequest.execute(), but the response is
1524 mocked and not really from an HTTP request/response.
1525 """
1526 return self.postproc(self.resp, self.content)
1527
1528
1529class RequestMockBuilder(object):
1530 """A simple mock of HttpRequest
1531
1532 Pass in a dictionary to the constructor that maps request methodIds to
1533 tuples of (httplib2.Response, content, opt_expected_body) that should be
1534 returned when that method is called. None may also be passed in for the
1535 httplib2.Response, in which case a 200 OK response will be generated.
1536 If an opt_expected_body (str or dict) is provided, it will be compared to
1537 the body and UnexpectedBodyError will be raised on inequality.
1538
1539 Example:
1540 response = '{"data": {"id": "tag:google.c...'
1541 requestBuilder = RequestMockBuilder(
1542 {
1543 'plus.activities.get': (None, response),
1544 }
1545 )
1546 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1547
1548 Methods that you do not supply a response for will return a
1549 200 OK with an empty string as the response content or raise an excpetion
1550 if check_unexpected is set to True. The methodId is taken from the rpcName
1551 in the discovery document.
1552
1553 For more details see the project wiki.
1554 """
1555
1556 def __init__(self, responses, check_unexpected=False):
1557 """Constructor for RequestMockBuilder
1558
1559 The constructed object should be a callable object
1560 that can replace the class HttpResponse.
1561
1562 responses - A dictionary that maps methodIds into tuples
1563 of (httplib2.Response, content). The methodId
1564 comes from the 'rpcName' field in the discovery
1565 document.
1566 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1567 should be raised on unsupplied method.
1568 """
1569 self.responses = responses
1570 self.check_unexpected = check_unexpected
1571
1572 def __call__(self, http, postproc, uri, method='GET', body=None,
1573 headers=None, methodId=None, resumable=None):
1574 """Implements the callable interface that discovery.build() expects
1575 of requestBuilder, which is to build an object compatible with
1576 HttpRequest.execute(). See that method for the description of the
1577 parameters and the expected response.
1578 """
1579 if methodId in self.responses:
1580 response = self.responses[methodId]
1581 resp, content = response[:2]
1582 if len(response) > 2:
1583 # Test the body against the supplied expected_body.
1584 expected_body = response[2]
1585 if bool(expected_body) != bool(body):
1586 # Not expecting a body and provided one
1587 # or expecting a body and not provided one.
1588 raise UnexpectedBodyError(expected_body, body)
1589 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001590 expected_body = json.loads(expected_body)
1591 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001592 if body != expected_body:
1593 raise UnexpectedBodyError(expected_body, body)
1594 return HttpRequestMock(resp, content, postproc)
1595 elif self.check_unexpected:
1596 raise UnexpectedMethodError(methodId=methodId)
1597 else:
1598 model = JsonModel(False)
1599 return HttpRequestMock(None, '{}', model.response)
1600
1601
1602class HttpMock(object):
1603 """Mock of httplib2.Http"""
1604
1605 def __init__(self, filename=None, headers=None):
1606 """
1607 Args:
1608 filename: string, absolute filename to read response from
1609 headers: dict, header to return with response
1610 """
1611 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001612 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001613 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001614 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001615 self.data = f.read()
1616 f.close()
1617 else:
1618 self.data = None
1619 self.response_headers = headers
1620 self.headers = None
1621 self.uri = None
1622 self.method = None
1623 self.body = None
1624 self.headers = None
1625
1626
1627 def request(self, uri,
1628 method='GET',
1629 body=None,
1630 headers=None,
1631 redirections=1,
1632 connection_type=None):
1633 self.uri = uri
1634 self.method = method
1635 self.body = body
1636 self.headers = headers
1637 return httplib2.Response(self.response_headers), self.data
1638
1639
1640class HttpMockSequence(object):
1641 """Mock of httplib2.Http
1642
1643 Mocks a sequence of calls to request returning different responses for each
1644 call. Create an instance initialized with the desired response headers
1645 and content and then use as if an httplib2.Http instance.
1646
1647 http = HttpMockSequence([
1648 ({'status': '401'}, ''),
1649 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1650 ({'status': '200'}, 'echo_request_headers'),
1651 ])
1652 resp, content = http.request("http://examples.com")
1653
1654 There are special values you can pass in for content to trigger
1655 behavours that are helpful in testing.
1656
1657 'echo_request_headers' means return the request headers in the response body
1658 'echo_request_headers_as_json' means return the request headers in
1659 the response body
1660 'echo_request_body' means return the request body in the response body
1661 'echo_request_uri' means return the request uri in the response body
1662 """
1663
1664 def __init__(self, iterable):
1665 """
1666 Args:
1667 iterable: iterable, a sequence of pairs of (headers, body)
1668 """
1669 self._iterable = iterable
1670 self.follow_redirects = True
1671
1672 def request(self, uri,
1673 method='GET',
1674 body=None,
1675 headers=None,
1676 redirections=1,
1677 connection_type=None):
1678 resp, content = self._iterable.pop(0)
1679 if content == 'echo_request_headers':
1680 content = headers
1681 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001682 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001683 elif content == 'echo_request_body':
1684 if hasattr(body, 'read'):
1685 content = body.read()
1686 else:
1687 content = body
1688 elif content == 'echo_request_uri':
1689 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001690 if isinstance(content, six.text_type):
1691 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001692 return httplib2.Response(resp), content
1693
1694
1695def set_user_agent(http, user_agent):
1696 """Set the user-agent on every request.
1697
1698 Args:
1699 http - An instance of httplib2.Http
1700 or something that acts like it.
1701 user_agent: string, the value for the user-agent header.
1702
1703 Returns:
1704 A modified instance of http that was passed in.
1705
1706 Example:
1707
1708 h = httplib2.Http()
1709 h = set_user_agent(h, "my-app-name/6.0")
1710
1711 Most of the time the user-agent will be set doing auth, this is for the rare
1712 cases where you are accessing an unauthenticated endpoint.
1713 """
1714 request_orig = http.request
1715
1716 # The closure that will replace 'httplib2.Http.request'.
1717 def new_request(uri, method='GET', body=None, headers=None,
1718 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1719 connection_type=None):
1720 """Modify the request headers to add the user-agent."""
1721 if headers is None:
1722 headers = {}
1723 if 'user-agent' in headers:
1724 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1725 else:
1726 headers['user-agent'] = user_agent
Bu Sun Kim1d2e2402019-05-17 16:03:03 -07001727 resp, content = request_orig(uri, method=method, body=body, headers=headers,
1728 redirections=redirections, connection_type=connection_type)
John Asmuth864311d2014-04-24 15:46:08 -04001729 return resp, content
1730
1731 http.request = new_request
1732 return http
1733
1734
1735def tunnel_patch(http):
1736 """Tunnel PATCH requests over POST.
1737 Args:
1738 http - An instance of httplib2.Http
1739 or something that acts like it.
1740
1741 Returns:
1742 A modified instance of http that was passed in.
1743
1744 Example:
1745
1746 h = httplib2.Http()
1747 h = tunnel_patch(h, "my-app-name/6.0")
1748
1749 Useful if you are running on a platform that doesn't support PATCH.
1750 Apply this last if you are using OAuth 1.0, as changing the method
1751 will result in a different signature.
1752 """
1753 request_orig = http.request
1754
1755 # The closure that will replace 'httplib2.Http.request'.
1756 def new_request(uri, method='GET', body=None, headers=None,
1757 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1758 connection_type=None):
1759 """Modify the request headers to add the user-agent."""
1760 if headers is None:
1761 headers = {}
1762 if method == 'PATCH':
1763 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001764 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001765 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1766 headers['x-http-method-override'] = "PATCH"
1767 method = 'POST'
Bu Sun Kim1d2e2402019-05-17 16:03:03 -07001768 resp, content = request_orig(uri, method=method, body=body, headers=headers,
1769 redirections=redirections, connection_type=connection_type)
John Asmuth864311d2014-04-24 15:46:08 -04001770 return resp, content
1771
1772 http.request = new_request
1773 return http
Igor Maravić22435292017-01-19 22:28:22 +01001774
1775
1776def build_http():
1777 """Builds httplib2.Http object
1778
1779 Returns:
1780 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1781 To override default timeout call
1782
1783 socket.setdefaulttimeout(timeout_in_sec)
1784
1785 before interacting with this method.
1786 """
1787 if socket.getdefaulttimeout() is not None:
1788 http_timeout = socket.getdefaulttimeout()
1789 else:
1790 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1791 return httplib2.Http(timeout=http_timeout)