blob: b8e1b8eaaee6bd6f809fc8e4dacd0a9300253b1b [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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070026__author__ = "jcgregorio@google.com (Joe Gregorio)"
John Asmuth864311d2014-04-24 15:46:08 -040027
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:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070047 import ssl
Tay Ray Chuan3146c922016-04-20 16:38:19 +000048except ImportError:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070049 _ssl_SSLError = object()
Tay Ray Chuan3146c922016-04-20 16:38:19 +000050else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070051 _ssl_SSLError = ssl.SSLError
Tay Ray Chuan3146c922016-04-20 16:38:19 +000052
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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070072DEFAULT_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
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070082_LEGACY_BATCH_URI = "https://www.googleapis.com/batch"
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070083
Damian Gadomskic7516a22020-03-23 20:39:21 +010084if six.PY2:
85 # That's a builtin python3 exception, nonexistent in python2.
86 # Defined to None to avoid NameError while trying to catch it
87 ConnectionError = None
88
eesheeshc6425a02016-02-12 15:07:06 +000089
90def _should_retry_response(resp_status, content):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070091 """Determines whether a response should be retried.
eesheeshc6425a02016-02-12 15:07:06 +000092
93 Args:
94 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080095 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000096
97 Returns:
98 True if the response should be retried, otherwise False.
99 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700100 # Retry on 5xx errors.
101 if resp_status >= 500:
102 return True
eesheeshc6425a02016-02-12 15:07:06 +0000103
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700104 # Retry on 429 errors.
105 if resp_status == _TOO_MANY_REQUESTS:
106 return True
eesheeshc6425a02016-02-12 15:07:06 +0000107
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700108 # For 403 errors, we have to check for the `reason` in the response to
109 # determine if we should retry.
110 if resp_status == six.moves.http_client.FORBIDDEN:
111 # If there's no details about the 403 type, don't retry.
112 if not content:
113 return False
eesheeshc6425a02016-02-12 15:07:06 +0000114
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700115 # Content is in JSON format.
116 try:
117 data = json.loads(content.decode("utf-8"))
118 if isinstance(data, dict):
Kapil Thangaveluc6912832020-12-02 14:52:02 -0500119 reason = data["error"].get("status")
120 if reason is None:
121 reason = data["error"]["errors"][0]["reason"]
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700122 else:
123 reason = data[0]["error"]["errors"]["reason"]
124 except (UnicodeDecodeError, ValueError, KeyError):
125 LOGGER.warning("Invalid JSON content from response: %s", content)
126 return False
eesheeshc6425a02016-02-12 15:07:06 +0000127
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700128 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
eesheeshc6425a02016-02-12 15:07:06 +0000129
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700130 # Only retry on rate limit related failures.
131 if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
132 return True
eesheeshc6425a02016-02-12 15:07:06 +0000133
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700134 # Everything else is a success or non-retriable so break.
135 return False
eesheeshc6425a02016-02-12 15:07:06 +0000136
John Asmuth864311d2014-04-24 15:46:08 -0400137
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700138def _retry_request(
139 http, num_retries, req_type, sleep, rand, uri, method, *args, **kwargs
140):
141 """Retries an HTTP request multiple times while handling errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100142
143 If after all retries the request still fails, last error is either returned as
144 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
145
146 Args:
147 http: Http object to be used to execute request.
148 num_retries: Maximum number of retries.
149 req_type: Type of the request (used for logging retries).
150 sleep, rand: Functions to sleep for random time between retries.
151 uri: URI to be requested.
152 method: HTTP method to be used.
153 args, kwargs: Additional arguments passed to http.request.
154
155 Returns:
156 resp, content - Response from the http request (may be HTTP 5xx).
157 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700158 resp = None
159 content = None
160 exception = None
161 for retry_num in range(num_retries + 1):
162 if retry_num > 0:
163 # Sleep before retrying.
164 sleep_time = rand() * 2 ** retry_num
165 LOGGER.warning(
166 "Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s",
167 sleep_time,
168 retry_num,
169 num_retries,
170 req_type,
171 method,
172 uri,
173 resp.status if resp else exception,
174 )
175 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100176
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700177 try:
178 exception = None
179 resp, content = http.request(uri, method, *args, **kwargs)
180 # Retry on SSL errors and socket timeout errors.
181 except _ssl_SSLError as ssl_error:
182 exception = ssl_error
183 except socket.timeout as socket_timeout:
184 # It's important that this be before socket.error as it's a subclass
185 # socket.timeout has no errorcode
186 exception = socket_timeout
Damian Gadomskic7516a22020-03-23 20:39:21 +0100187 except ConnectionError as connection_error:
188 # Needs to be before socket.error as it's a subclass of
189 # OSError (socket.error)
190 exception = connection_error
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700191 except socket.error as socket_error:
192 # errno's contents differ by platform, so we have to match by name.
193 if socket.errno.errorcode.get(socket_error.errno) not in {
194 "WSAETIMEDOUT",
195 "ETIMEDOUT",
196 "EPIPE",
197 "ECONNABORTED",
198 }:
199 raise
200 exception = socket_error
201 except httplib2.ServerNotFoundError as server_not_found_error:
202 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000203
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700204 if exception:
205 if retry_num == num_retries:
206 raise exception
207 else:
208 continue
eesheeshc6425a02016-02-12 15:07:06 +0000209
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700210 if not _should_retry_response(resp.status, content):
211 break
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100212
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700213 return resp, content
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100214
215
John Asmuth864311d2014-04-24 15:46:08 -0400216class MediaUploadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700217 """Status of a resumable upload."""
John Asmuth864311d2014-04-24 15:46:08 -0400218
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700219 def __init__(self, resumable_progress, total_size):
220 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400221
222 Args:
223 resumable_progress: int, bytes sent so far.
224 total_size: int, total bytes in complete upload, or None if the total
225 upload size isn't known ahead of time.
226 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700227 self.resumable_progress = resumable_progress
228 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400229
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700230 def progress(self):
231 """Percent of upload completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400232
233 Returns:
234 the percentage complete as a float, returning 0.0 if the total size of
235 the upload is unknown.
236 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700237 if self.total_size is not None and self.total_size != 0:
238 return float(self.resumable_progress) / float(self.total_size)
239 else:
240 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400241
242
243class MediaDownloadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700244 """Status of a resumable download."""
John Asmuth864311d2014-04-24 15:46:08 -0400245
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700246 def __init__(self, resumable_progress, total_size):
247 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400248
249 Args:
250 resumable_progress: int, bytes received so far.
251 total_size: int, total bytes in complete download.
252 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700253 self.resumable_progress = resumable_progress
254 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400255
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700256 def progress(self):
257 """Percent of download completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400258
259 Returns:
260 the percentage complete as a float, returning 0.0 if the total size of
261 the download is unknown.
262 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700263 if self.total_size is not None and self.total_size != 0:
264 return float(self.resumable_progress) / float(self.total_size)
265 else:
266 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400267
268
269class MediaUpload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700270 """Describes a media object to upload.
John Asmuth864311d2014-04-24 15:46:08 -0400271
272 Base class that defines the interface of MediaUpload subclasses.
273
274 Note that subclasses of MediaUpload may allow you to control the chunksize
275 when uploading a media object. It is important to keep the size of the chunk
276 as large as possible to keep the upload efficient. Other factors may influence
277 the size of the chunk you use, particularly if you are working in an
278 environment where individual HTTP requests may have a hardcoded time limit,
279 such as under certain classes of requests under Google App Engine.
280
281 Streams are io.Base compatible objects that support seek(). Some MediaUpload
282 subclasses support using streams directly to upload data. Support for
283 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
284 platform that stream will be used for uploading the media object. The support
285 for streaming is indicated by has_stream() returning True. The stream() method
286 should return an io.Base object that supports seek(). On platforms where the
287 underlying httplib module supports streaming, for example Python 2.6 and
288 later, the stream will be passed into the http library which will result in
289 less memory being used and possibly faster uploads.
290
291 If you need to upload media that can't be uploaded using any of the existing
292 MediaUpload sub-class then you can sub-class MediaUpload for your particular
293 needs.
294 """
295
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700296 def chunksize(self):
297 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400298
299 Returns:
300 Chunk size in bytes.
301 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400303
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700304 def mimetype(self):
305 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400306
307 Returns:
308 Mime type.
309 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700310 return "application/octet-stream"
John Asmuth864311d2014-04-24 15:46:08 -0400311
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700312 def size(self):
313 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400314
315 Returns:
316 Size of the body, or None of the size is unknown.
317 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700318 return None
John Asmuth864311d2014-04-24 15:46:08 -0400319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700320 def resumable(self):
321 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400322
323 Returns:
324 True if resumable upload or False.
325 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700326 return False
John Asmuth864311d2014-04-24 15:46:08 -0400327
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700328 def getbytes(self, begin, end):
329 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400330
331 Args:
332 begin: int, offset from beginning of file.
333 length: int, number of bytes to read, starting at begin.
334
335 Returns:
336 A string of bytes read. May be shorter than length if EOF was reached
337 first.
338 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700339 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400340
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700341 def has_stream(self):
342 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400343
344 Streaming means it is an io.IOBase subclass that supports seek, i.e.
345 seekable() returns True.
346
347 Returns:
348 True if the call to stream() will return an instance of a seekable io.Base
349 subclass.
350 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700351 return False
John Asmuth864311d2014-04-24 15:46:08 -0400352
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700353 def stream(self):
354 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400355
356 Returns:
357 The returned value is an io.IOBase subclass that supports seek, i.e.
358 seekable() returns True.
359 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700360 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400361
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700362 @util.positional(1)
363 def _to_json(self, strip=None):
364 """Utility function for creating a JSON representation of a MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400365
366 Args:
367 strip: array, An array of names of members to not include in the JSON.
368
369 Returns:
370 string, a JSON representation of this instance, suitable to pass to
371 from_json().
372 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700373 t = type(self)
374 d = copy.copy(self.__dict__)
375 if strip is not None:
376 for member in strip:
377 del d[member]
378 d["_class"] = t.__name__
379 d["_module"] = t.__module__
380 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400381
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700382 def to_json(self):
383 """Create a JSON representation of an instance of MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400384
385 Returns:
386 string, a JSON representation of this instance, suitable to pass to
387 from_json().
388 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700389 return self._to_json()
John Asmuth864311d2014-04-24 15:46:08 -0400390
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700391 @classmethod
392 def new_from_json(cls, s):
393 """Utility class method to instantiate a MediaUpload subclass from a JSON
John Asmuth864311d2014-04-24 15:46:08 -0400394 representation produced by to_json().
395
396 Args:
397 s: string, JSON from to_json().
398
399 Returns:
400 An instance of the subclass of MediaUpload that was serialized with
401 to_json().
402 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700403 data = json.loads(s)
404 # Find and call the right classmethod from_json() to restore the object.
405 module = data["_module"]
406 m = __import__(module, fromlist=module.split(".")[:-1])
407 kls = getattr(m, data["_class"])
408 from_json = getattr(kls, "from_json")
409 return from_json(s)
John Asmuth864311d2014-04-24 15:46:08 -0400410
411
412class MediaIoBaseUpload(MediaUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700413 """A MediaUpload for a io.Base objects.
John Asmuth864311d2014-04-24 15:46:08 -0400414
415 Note that the Python file object is compatible with io.Base and can be used
416 with this class also.
417
Pat Ferateed9affd2015-03-03 16:03:15 -0800418 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400419 media = MediaIoBaseUpload(fh, mimetype='image/png',
420 chunksize=1024*1024, resumable=True)
421 farm.animals().insert(
422 id='cow',
423 name='cow.png',
424 media_body=media).execute()
425
426 Depending on the platform you are working on, you may pass -1 as the
427 chunksize, which indicates that the entire file should be uploaded in a single
428 request. If the underlying platform supports streams, such as Python 2.6 or
429 later, then this can be very efficient as it avoids multiple connections, and
430 also avoids loading the entire file into memory before sending it. Note that
431 Google App Engine has a 5MB limit on request size, so you should never set
432 your chunksize larger than 5MB, or to -1.
433 """
434
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700435 @util.positional(3)
436 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
437 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400438
439 Args:
440 fd: io.Base or file object, The source of the bytes to upload. MUST be
441 opened in blocking mode, do not use streams opened in non-blocking mode.
442 The given stream must be seekable, that is, it must be able to call
443 seek() on fd.
444 mimetype: string, Mime-type of the file.
445 chunksize: int, File will be uploaded in chunks of this many bytes. Only
446 used if resumable=True. Pass in a value of -1 if the file is to be
447 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
448 on request size, so you should never set your chunksize larger than 5MB,
449 or to -1.
450 resumable: bool, True if this is a resumable upload. False means upload
451 in a single request.
452 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700453 super(MediaIoBaseUpload, self).__init__()
454 self._fd = fd
455 self._mimetype = mimetype
456 if not (chunksize == -1 or chunksize > 0):
457 raise InvalidChunkSizeError()
458 self._chunksize = chunksize
459 self._resumable = resumable
John Asmuth864311d2014-04-24 15:46:08 -0400460
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700461 self._fd.seek(0, os.SEEK_END)
462 self._size = self._fd.tell()
John Asmuth864311d2014-04-24 15:46:08 -0400463
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700464 def chunksize(self):
465 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400466
467 Returns:
468 Chunk size in bytes.
469 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700470 return self._chunksize
John Asmuth864311d2014-04-24 15:46:08 -0400471
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700472 def mimetype(self):
473 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400474
475 Returns:
476 Mime type.
477 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700478 return self._mimetype
John Asmuth864311d2014-04-24 15:46:08 -0400479
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700480 def size(self):
481 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400482
483 Returns:
484 Size of the body, or None of the size is unknown.
485 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700486 return self._size
John Asmuth864311d2014-04-24 15:46:08 -0400487
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700488 def resumable(self):
489 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400490
491 Returns:
492 True if resumable upload or False.
493 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700494 return self._resumable
John Asmuth864311d2014-04-24 15:46:08 -0400495
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700496 def getbytes(self, begin, length):
497 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400498
499 Args:
500 begin: int, offset from beginning of file.
501 length: int, number of bytes to read, starting at begin.
502
503 Returns:
504 A string of bytes read. May be shorted than length if EOF was reached
505 first.
506 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700507 self._fd.seek(begin)
508 return self._fd.read(length)
John Asmuth864311d2014-04-24 15:46:08 -0400509
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700510 def has_stream(self):
511 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400512
513 Streaming means it is an io.IOBase subclass that supports seek, i.e.
514 seekable() returns True.
515
516 Returns:
517 True if the call to stream() will return an instance of a seekable io.Base
518 subclass.
519 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700520 return True
John Asmuth864311d2014-04-24 15:46:08 -0400521
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700522 def stream(self):
523 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400524
525 Returns:
526 The returned value is an io.IOBase subclass that supports seek, i.e.
527 seekable() returns True.
528 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700529 return self._fd
John Asmuth864311d2014-04-24 15:46:08 -0400530
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700531 def to_json(self):
532 """This upload type is not serializable."""
533 raise NotImplementedError("MediaIoBaseUpload is not serializable.")
John Asmuth864311d2014-04-24 15:46:08 -0400534
535
536class MediaFileUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700537 """A MediaUpload for a file.
John Asmuth864311d2014-04-24 15:46:08 -0400538
539 Construct a MediaFileUpload and pass as the media_body parameter of the
540 method. For example, if we had a service that allowed uploading images:
541
John Asmuth864311d2014-04-24 15:46:08 -0400542 media = MediaFileUpload('cow.png', mimetype='image/png',
543 chunksize=1024*1024, resumable=True)
544 farm.animals().insert(
545 id='cow',
546 name='cow.png',
547 media_body=media).execute()
548
549 Depending on the platform you are working on, you may pass -1 as the
550 chunksize, which indicates that the entire file should be uploaded in a single
551 request. If the underlying platform supports streams, such as Python 2.6 or
552 later, then this can be very efficient as it avoids multiple connections, and
553 also avoids loading the entire file into memory before sending it. Note that
554 Google App Engine has a 5MB limit on request size, so you should never set
555 your chunksize larger than 5MB, or to -1.
556 """
557
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700558 @util.positional(2)
559 def __init__(
560 self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False
561 ):
562 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400563
564 Args:
565 filename: string, Name of the file.
566 mimetype: string, Mime-type of the file. If None then a mime-type will be
567 guessed from the file extension.
568 chunksize: int, File will be uploaded in chunks of this many bytes. Only
569 used if resumable=True. Pass in a value of -1 if the file is to be
570 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
571 on request size, so you should never set your chunksize larger than 5MB,
572 or to -1.
573 resumable: bool, True if this is a resumable upload. False means upload
574 in a single request.
575 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700576 self._filename = filename
577 fd = open(self._filename, "rb")
578 if mimetype is None:
579 # No mimetype provided, make a guess.
580 mimetype, _ = mimetypes.guess_type(filename)
581 if mimetype is None:
582 # Guess failed, use octet-stream.
583 mimetype = "application/octet-stream"
584 super(MediaFileUpload, self).__init__(
585 fd, mimetype, chunksize=chunksize, resumable=resumable
586 )
John Asmuth864311d2014-04-24 15:46:08 -0400587
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700588 def __del__(self):
589 self._fd.close()
Xiaofei Wang20b67582019-07-17 11:16:53 -0700590
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700591 def to_json(self):
592 """Creating a JSON representation of an instance of MediaFileUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400593
594 Returns:
595 string, a JSON representation of this instance, suitable to pass to
596 from_json().
597 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700598 return self._to_json(strip=["_fd"])
John Asmuth864311d2014-04-24 15:46:08 -0400599
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700600 @staticmethod
601 def from_json(s):
602 d = json.loads(s)
603 return MediaFileUpload(
604 d["_filename"],
605 mimetype=d["_mimetype"],
606 chunksize=d["_chunksize"],
607 resumable=d["_resumable"],
608 )
John Asmuth864311d2014-04-24 15:46:08 -0400609
610
611class MediaInMemoryUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700612 """MediaUpload for a chunk of bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400613
614 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
615 the stream.
616 """
617
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700618 @util.positional(2)
619 def __init__(
620 self,
621 body,
622 mimetype="application/octet-stream",
623 chunksize=DEFAULT_CHUNK_SIZE,
624 resumable=False,
625 ):
626 """Create a new MediaInMemoryUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400627
628 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
629 the stream.
630
631 Args:
632 body: string, Bytes of body content.
633 mimetype: string, Mime-type of the file or default of
634 'application/octet-stream'.
635 chunksize: int, File will be uploaded in chunks of this many bytes. Only
636 used if resumable=True.
637 resumable: bool, True if this is a resumable upload. False means upload
638 in a single request.
639 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700640 fd = BytesIO(body)
641 super(MediaInMemoryUpload, self).__init__(
642 fd, mimetype, chunksize=chunksize, resumable=resumable
643 )
John Asmuth864311d2014-04-24 15:46:08 -0400644
645
646class MediaIoBaseDownload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700647 """"Download media resources.
John Asmuth864311d2014-04-24 15:46:08 -0400648
649 Note that the Python file object is compatible with io.Base and can be used
650 with this class also.
651
652
653 Example:
654 request = farms.animals().get_media(id='cow')
655 fh = io.FileIO('cow.png', mode='wb')
656 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
657
658 done = False
659 while done is False:
660 status, done = downloader.next_chunk()
661 if status:
662 print "Download %d%%." % int(status.progress() * 100)
663 print "Download Complete!"
664 """
665
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700666 @util.positional(3)
667 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
668 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400669
670 Args:
671 fd: io.Base or file object, The stream in which to write the downloaded
672 bytes.
673 request: googleapiclient.http.HttpRequest, the media request to perform in
674 chunks.
675 chunksize: int, File will be downloaded in chunks of this many bytes.
676 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700677 self._fd = fd
678 self._request = request
679 self._uri = request.uri
680 self._chunksize = chunksize
681 self._progress = 0
682 self._total_size = None
683 self._done = False
John Asmuth864311d2014-04-24 15:46:08 -0400684
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700685 # Stubs for testing.
686 self._sleep = time.sleep
687 self._rand = random.random
John Asmuth864311d2014-04-24 15:46:08 -0400688
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700689 self._headers = {}
690 for k, v in six.iteritems(request.headers):
691 # allow users to supply custom headers by setting them on the request
692 # but strip out the ones that are set by default on requests generated by
693 # API methods like Drive's files().get(fileId=...)
694 if not k.lower() in ("accept", "accept-encoding", "user-agent"):
695 self._headers[k] = v
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400696
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700697 @util.positional(1)
698 def next_chunk(self, num_retries=0):
699 """Get the next chunk of the download.
John Asmuth864311d2014-04-24 15:46:08 -0400700
701 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500702 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400703 exponential backoff. If all retries fail, the raised HttpError
704 represents the last request. If zero (default), we attempt the
705 request only once.
706
707 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700708 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400709 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100710 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400711
712 Raises:
713 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000714 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400715 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700716 headers = self._headers.copy()
717 headers["range"] = "bytes=%d-%d" % (
718 self._progress,
719 self._progress + self._chunksize,
720 )
721 http = self._request.http
John Asmuth864311d2014-04-24 15:46:08 -0400722
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700723 resp, content = _retry_request(
724 http,
725 num_retries,
726 "media download",
727 self._sleep,
728 self._rand,
729 self._uri,
730 "GET",
731 headers=headers,
732 )
John Asmuth864311d2014-04-24 15:46:08 -0400733
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700734 if resp.status in [200, 206]:
735 if "content-location" in resp and resp["content-location"] != self._uri:
736 self._uri = resp["content-location"]
737 self._progress += len(content)
738 self._fd.write(content)
John Asmuth864311d2014-04-24 15:46:08 -0400739
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700740 if "content-range" in resp:
741 content_range = resp["content-range"]
742 length = content_range.rsplit("/", 1)[1]
743 self._total_size = int(length)
744 elif "content-length" in resp:
745 self._total_size = int(resp["content-length"])
John Asmuth864311d2014-04-24 15:46:08 -0400746
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700747 if self._total_size is None or self._progress == self._total_size:
748 self._done = True
749 return MediaDownloadProgress(self._progress, self._total_size), self._done
Bu Sun Kim86d87882020-10-22 08:51:16 -0600750 elif resp.status == 416:
751 # 416 is Range Not Satisfiable
752 # This typically occurs with a zero byte file
753 content_range = resp["content-range"]
754 length = content_range.rsplit("/", 1)[1]
755 self._total_size = int(length)
756 if self._total_size == 0:
757 self._done = True
758 return MediaDownloadProgress(self._progress, self._total_size), self._done
759 raise HttpError(resp, content, uri=self._uri)
John Asmuth864311d2014-04-24 15:46:08 -0400760
761
762class _StreamSlice(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700763 """Truncated stream.
John Asmuth864311d2014-04-24 15:46:08 -0400764
765 Takes a stream and presents a stream that is a slice of the original stream.
766 This is used when uploading media in chunks. In later versions of Python a
767 stream can be passed to httplib in place of the string of data to send. The
768 problem is that httplib just blindly reads to the end of the stream. This
769 wrapper presents a virtual stream that only reads to the end of the chunk.
770 """
771
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700772 def __init__(self, stream, begin, chunksize):
773 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400774
775 Args:
776 stream: (io.Base, file object), the stream to wrap.
777 begin: int, the seek position the chunk begins at.
778 chunksize: int, the size of the chunk.
779 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700780 self._stream = stream
781 self._begin = begin
782 self._chunksize = chunksize
783 self._stream.seek(begin)
John Asmuth864311d2014-04-24 15:46:08 -0400784
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700785 def read(self, n=-1):
786 """Read n bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400787
788 Args:
789 n, int, the number of bytes to read.
790
791 Returns:
792 A string of length 'n', or less if EOF is reached.
793 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700794 # The data left available to read sits in [cur, end)
795 cur = self._stream.tell()
796 end = self._begin + self._chunksize
797 if n == -1 or cur + n > end:
798 n = end - cur
799 return self._stream.read(n)
John Asmuth864311d2014-04-24 15:46:08 -0400800
801
802class HttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700803 """Encapsulates a single HTTP request."""
John Asmuth864311d2014-04-24 15:46:08 -0400804
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700805 @util.positional(4)
806 def __init__(
807 self,
808 http,
809 postproc,
810 uri,
811 method="GET",
812 body=None,
813 headers=None,
814 methodId=None,
815 resumable=None,
816 ):
817 """Constructor for an HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -0400818
819 Args:
820 http: httplib2.Http, the transport object to use to make a request
821 postproc: callable, called on the HTTP response and content to transform
822 it into a data object before returning, or raising an exception
823 on an error.
824 uri: string, the absolute URI to send the request to
825 method: string, the HTTP method to use
826 body: string, the request body of the HTTP request,
827 headers: dict, the HTTP request headers
828 methodId: string, a unique identifier for the API method being called.
829 resumable: MediaUpload, None if this is not a resumbale request.
830 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700831 self.uri = uri
832 self.method = method
833 self.body = body
834 self.headers = headers or {}
835 self.methodId = methodId
836 self.http = http
837 self.postproc = postproc
838 self.resumable = resumable
839 self.response_callbacks = []
840 self._in_error_state = False
John Asmuth864311d2014-04-24 15:46:08 -0400841
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700842 # The size of the non-media part of the request.
843 self.body_size = len(self.body or "")
John Asmuth864311d2014-04-24 15:46:08 -0400844
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700845 # The resumable URI to send chunks to.
846 self.resumable_uri = None
John Asmuth864311d2014-04-24 15:46:08 -0400847
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700848 # The bytes that have been uploaded.
849 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -0400850
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700851 # Stubs for testing.
852 self._rand = random.random
853 self._sleep = time.sleep
John Asmuth864311d2014-04-24 15:46:08 -0400854
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700855 @util.positional(1)
856 def execute(self, http=None, num_retries=0):
857 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -0400858
859 Args:
860 http: httplib2.Http, an http object to be used in place of the
861 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500862 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400863 exponential backoff. If all retries fail, the raised HttpError
864 represents the last request. If zero (default), we attempt the
865 request only once.
866
867 Returns:
868 A deserialized object model of the response body as determined
869 by the postproc.
870
871 Raises:
872 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000873 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400874 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700875 if http is None:
876 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400877
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700878 if self.resumable:
879 body = None
880 while body is None:
881 _, body = self.next_chunk(http=http, num_retries=num_retries)
882 return body
John Asmuth864311d2014-04-24 15:46:08 -0400883
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700884 # Non-resumable case.
John Asmuth864311d2014-04-24 15:46:08 -0400885
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700886 if "content-length" not in self.headers:
887 self.headers["content-length"] = str(self.body_size)
888 # If the request URI is too long then turn it into a POST request.
889 # Assume that a GET request never contains a request body.
890 if len(self.uri) > MAX_URI_LENGTH and self.method == "GET":
891 self.method = "POST"
892 self.headers["x-http-method-override"] = "GET"
893 self.headers["content-type"] = "application/x-www-form-urlencoded"
894 parsed = urlparse(self.uri)
895 self.uri = urlunparse(
896 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
897 )
898 self.body = parsed.query
899 self.headers["content-length"] = str(len(self.body))
John Asmuth864311d2014-04-24 15:46:08 -0400900
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700901 # Handle retries for server-side errors.
902 resp, content = _retry_request(
903 http,
904 num_retries,
905 "request",
906 self._sleep,
907 self._rand,
908 str(self.uri),
909 method=str(self.method),
910 body=self.body,
911 headers=self.headers,
912 )
John Asmuth864311d2014-04-24 15:46:08 -0400913
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700914 for callback in self.response_callbacks:
915 callback(resp)
916 if resp.status >= 300:
917 raise HttpError(resp, content, uri=self.uri)
918 return self.postproc(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400919
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700920 @util.positional(2)
921 def add_response_callback(self, cb):
922 """add_response_headers_callback
John Asmuth864311d2014-04-24 15:46:08 -0400923
924 Args:
925 cb: Callback to be called on receiving the response headers, of signature:
926
927 def cb(resp):
928 # Where resp is an instance of httplib2.Response
929 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700930 self.response_callbacks.append(cb)
John Asmuth864311d2014-04-24 15:46:08 -0400931
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700932 @util.positional(1)
933 def next_chunk(self, http=None, num_retries=0):
934 """Execute the next step of a resumable upload.
John Asmuth864311d2014-04-24 15:46:08 -0400935
936 Can only be used if the method being executed supports media uploads and
937 the MediaUpload object passed in was flagged as using resumable upload.
938
939 Example:
940
941 media = MediaFileUpload('cow.png', mimetype='image/png',
942 chunksize=1000, resumable=True)
943 request = farm.animals().insert(
944 id='cow',
945 name='cow.png',
946 media_body=media)
947
948 response = None
949 while response is None:
950 status, response = request.next_chunk()
951 if status:
952 print "Upload %d%% complete." % int(status.progress() * 100)
953
954
955 Args:
956 http: httplib2.Http, an http object to be used in place of the
957 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500958 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400959 exponential backoff. If all retries fail, the raised HttpError
960 represents the last request. If zero (default), we attempt the
961 request only once.
962
963 Returns:
964 (status, body): (ResumableMediaStatus, object)
965 The body will be None until the resumable media is fully uploaded.
966
967 Raises:
968 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000969 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400970 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700971 if http is None:
972 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400973
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700974 if self.resumable.size() is None:
975 size = "*"
976 else:
977 size = str(self.resumable.size())
John Asmuth864311d2014-04-24 15:46:08 -0400978
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700979 if self.resumable_uri is None:
980 start_headers = copy.copy(self.headers)
981 start_headers["X-Upload-Content-Type"] = self.resumable.mimetype()
982 if size != "*":
983 start_headers["X-Upload-Content-Length"] = size
984 start_headers["content-length"] = str(self.body_size)
John Asmuth864311d2014-04-24 15:46:08 -0400985
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700986 resp, content = _retry_request(
987 http,
988 num_retries,
989 "resumable URI request",
990 self._sleep,
991 self._rand,
992 self.uri,
993 method=self.method,
994 body=self.body,
995 headers=start_headers,
996 )
John Asmuth864311d2014-04-24 15:46:08 -0400997
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700998 if resp.status == 200 and "location" in resp:
999 self.resumable_uri = resp["location"]
1000 else:
1001 raise ResumableUploadError(resp, content)
1002 elif self._in_error_state:
1003 # If we are in an error state then query the server for current state of
1004 # the upload by sending an empty PUT and reading the 'range' header in
1005 # the response.
1006 headers = {"Content-Range": "bytes */%s" % size, "content-length": "0"}
1007 resp, content = http.request(self.resumable_uri, "PUT", headers=headers)
1008 status, body = self._process_response(resp, content)
1009 if body:
1010 # The upload was complete.
1011 return (status, body)
John Asmuth864311d2014-04-24 15:46:08 -04001012
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001013 if self.resumable.has_stream():
1014 data = self.resumable.stream()
1015 if self.resumable.chunksize() == -1:
1016 data.seek(self.resumable_progress)
1017 chunk_end = self.resumable.size() - self.resumable_progress - 1
1018 else:
1019 # Doing chunking with a stream, so wrap a slice of the stream.
1020 data = _StreamSlice(
1021 data, self.resumable_progress, self.resumable.chunksize()
1022 )
1023 chunk_end = min(
1024 self.resumable_progress + self.resumable.chunksize() - 1,
1025 self.resumable.size() - 1,
1026 )
1027 else:
1028 data = self.resumable.getbytes(
1029 self.resumable_progress, self.resumable.chunksize()
1030 )
John Asmuth864311d2014-04-24 15:46:08 -04001031
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001032 # A short read implies that we are at EOF, so finish the upload.
1033 if len(data) < self.resumable.chunksize():
1034 size = str(self.resumable_progress + len(data))
John Asmuth864311d2014-04-24 15:46:08 -04001035
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001036 chunk_end = self.resumable_progress + len(data) - 1
John Asmuth864311d2014-04-24 15:46:08 -04001037
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001038 headers = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001039 # Must set the content-length header here because httplib can't
1040 # calculate the size when working with _StreamSlice.
1041 "Content-Length": str(chunk_end - self.resumable_progress + 1),
John Asmuth864311d2014-04-24 15:46:08 -04001042 }
1043
Bu Sun Kimaf6035f2020-10-20 16:36:04 -06001044 # An empty file results in chunk_end = -1 and size = 0
1045 # sending "bytes 0--1/0" results in an invalid request
1046 # Only add header "Content-Range" if chunk_end != -1
1047 if chunk_end != -1:
1048 headers["Content-Range"] = "bytes %d-%d/%s" % (self.resumable_progress, chunk_end, size)
1049
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001050 for retry_num in range(num_retries + 1):
1051 if retry_num > 0:
1052 self._sleep(self._rand() * 2 ** retry_num)
1053 LOGGER.warning(
1054 "Retry #%d for media upload: %s %s, following status: %d"
1055 % (retry_num, self.method, self.uri, resp.status)
1056 )
John Asmuth864311d2014-04-24 15:46:08 -04001057
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001058 try:
1059 resp, content = http.request(
1060 self.resumable_uri, method="PUT", body=data, headers=headers
1061 )
1062 except:
1063 self._in_error_state = True
1064 raise
1065 if not _should_retry_response(resp.status, content):
1066 break
John Asmuth864311d2014-04-24 15:46:08 -04001067
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001068 return self._process_response(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -04001069
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001070 def _process_response(self, resp, content):
1071 """Process the response from a single chunk upload.
John Asmuth864311d2014-04-24 15:46:08 -04001072
1073 Args:
1074 resp: httplib2.Response, the response object.
1075 content: string, the content of the response.
1076
1077 Returns:
1078 (status, body): (ResumableMediaStatus, object)
1079 The body will be None until the resumable media is fully uploaded.
1080
1081 Raises:
1082 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1083 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001084 if resp.status in [200, 201]:
1085 self._in_error_state = False
1086 return None, self.postproc(resp, content)
1087 elif resp.status == 308:
1088 self._in_error_state = False
1089 # A "308 Resume Incomplete" indicates we are not done.
1090 try:
1091 self.resumable_progress = int(resp["range"].split("-")[1]) + 1
1092 except KeyError:
1093 # If resp doesn't contain range header, resumable progress is 0
1094 self.resumable_progress = 0
1095 if "location" in resp:
1096 self.resumable_uri = resp["location"]
1097 else:
1098 self._in_error_state = True
1099 raise HttpError(resp, content, uri=self.uri)
John Asmuth864311d2014-04-24 15:46:08 -04001100
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001101 return (
1102 MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1103 None,
1104 )
John Asmuth864311d2014-04-24 15:46:08 -04001105
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001106 def to_json(self):
1107 """Returns a JSON representation of the HttpRequest."""
1108 d = copy.copy(self.__dict__)
1109 if d["resumable"] is not None:
1110 d["resumable"] = self.resumable.to_json()
1111 del d["http"]
1112 del d["postproc"]
1113 del d["_sleep"]
1114 del d["_rand"]
John Asmuth864311d2014-04-24 15:46:08 -04001115
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001116 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001117
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001118 @staticmethod
1119 def from_json(s, http, postproc):
1120 """Returns an HttpRequest populated with info from a JSON object."""
1121 d = json.loads(s)
1122 if d["resumable"] is not None:
1123 d["resumable"] = MediaUpload.new_from_json(d["resumable"])
1124 return HttpRequest(
1125 http,
1126 postproc,
1127 uri=d["uri"],
1128 method=d["method"],
1129 body=d["body"],
1130 headers=d["headers"],
1131 methodId=d["methodId"],
1132 resumable=d["resumable"],
1133 )
John Asmuth864311d2014-04-24 15:46:08 -04001134
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001135 @staticmethod
1136 def null_postproc(resp, contents):
1137 return resp, contents
1138
John Asmuth864311d2014-04-24 15:46:08 -04001139
1140class BatchHttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001141 """Batches multiple HttpRequest objects into a single HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001142
1143 Example:
1144 from googleapiclient.http import BatchHttpRequest
1145
1146 def list_animals(request_id, response, exception):
1147 \"\"\"Do something with the animals list response.\"\"\"
1148 if exception is not None:
1149 # Do something with the exception.
1150 pass
1151 else:
1152 # Do something with the response.
1153 pass
1154
1155 def list_farmers(request_id, response, exception):
1156 \"\"\"Do something with the farmers list response.\"\"\"
1157 if exception is not None:
1158 # Do something with the exception.
1159 pass
1160 else:
1161 # Do something with the response.
1162 pass
1163
1164 service = build('farm', 'v2')
1165
1166 batch = BatchHttpRequest()
1167
1168 batch.add(service.animals().list(), list_animals)
1169 batch.add(service.farmers().list(), list_farmers)
1170 batch.execute(http=http)
1171 """
1172
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001173 @util.positional(1)
1174 def __init__(self, callback=None, batch_uri=None):
1175 """Constructor for a BatchHttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001176
1177 Args:
1178 callback: callable, A callback to be called for each response, of the
1179 form callback(id, response, exception). The first parameter is the
1180 request id, and the second is the deserialized response object. The
1181 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1182 occurred while processing the request, or None if no error occurred.
1183 batch_uri: string, URI to send batch requests to.
1184 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001185 if batch_uri is None:
1186 batch_uri = _LEGACY_BATCH_URI
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001187
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001188 if batch_uri == _LEGACY_BATCH_URI:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001189 LOGGER.warning(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001190 "You have constructed a BatchHttpRequest using the legacy batch "
Brad Vogel6ddadd72020-05-15 10:02:04 -07001191 "endpoint %s. This endpoint will be turned down on August 12, 2020. "
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001192 "Please provide the API-specific endpoint or use "
1193 "service.new_batch_http_request(). For more details see "
1194 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1195 "and https://developers.google.com/api-client-library/python/guide/batch.",
1196 _LEGACY_BATCH_URI,
1197 )
1198 self._batch_uri = batch_uri
John Asmuth864311d2014-04-24 15:46:08 -04001199
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001200 # Global callback to be called for each individual response in the batch.
1201 self._callback = callback
John Asmuth864311d2014-04-24 15:46:08 -04001202
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001203 # A map from id to request.
1204 self._requests = {}
John Asmuth864311d2014-04-24 15:46:08 -04001205
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001206 # A map from id to callback.
1207 self._callbacks = {}
John Asmuth864311d2014-04-24 15:46:08 -04001208
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001209 # List of request ids, in the order in which they were added.
1210 self._order = []
John Asmuth864311d2014-04-24 15:46:08 -04001211
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001212 # The last auto generated id.
1213 self._last_auto_id = 0
John Asmuth864311d2014-04-24 15:46:08 -04001214
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001215 # Unique ID on which to base the Content-ID headers.
1216 self._base_id = None
John Asmuth864311d2014-04-24 15:46:08 -04001217
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001218 # A map from request id to (httplib2.Response, content) response pairs
1219 self._responses = {}
John Asmuth864311d2014-04-24 15:46:08 -04001220
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001221 # A map of id(Credentials) that have been refreshed.
1222 self._refreshed_credentials = {}
John Asmuth864311d2014-04-24 15:46:08 -04001223
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001224 def _refresh_and_apply_credentials(self, request, http):
1225 """Refresh the credentials and apply to the request.
John Asmuth864311d2014-04-24 15:46:08 -04001226
1227 Args:
1228 request: HttpRequest, the request.
1229 http: httplib2.Http, the global http object for the batch.
1230 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001231 # For the credentials to refresh, but only once per refresh_token
1232 # If there is no http per the request then refresh the http passed in
1233 # via execute()
1234 creds = None
1235 request_credentials = False
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001236
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001237 if request.http is not None:
1238 creds = _auth.get_credentials_from_http(request.http)
1239 request_credentials = True
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001240
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001241 if creds is None and http is not None:
1242 creds = _auth.get_credentials_from_http(http)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001244 if creds is not None:
1245 if id(creds) not in self._refreshed_credentials:
1246 _auth.refresh_credentials(creds)
1247 self._refreshed_credentials[id(creds)] = 1
John Asmuth864311d2014-04-24 15:46:08 -04001248
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001249 # Only apply the credentials if we are using the http object passed in,
1250 # otherwise apply() will get called during _serialize_request().
1251 if request.http is None or not request_credentials:
1252 _auth.apply_credentials(creds, request.headers)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001253
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001254 def _id_to_header(self, id_):
1255 """Convert an id to a Content-ID header value.
John Asmuth864311d2014-04-24 15:46:08 -04001256
1257 Args:
1258 id_: string, identifier of individual request.
1259
1260 Returns:
1261 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1262 the value because Content-ID headers are supposed to be universally
1263 unique.
1264 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001265 if self._base_id is None:
1266 self._base_id = uuid.uuid4()
John Asmuth864311d2014-04-24 15:46:08 -04001267
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001268 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1269 # line folding works properly on Python 3; see
Marie J.I48f503f2020-05-15 13:32:11 -04001270 # https://github.com/googleapis/google-api-python-client/issues/164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001271 return "<%s + %s>" % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001272
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001273 def _header_to_id(self, header):
1274 """Convert a Content-ID header value to an id.
John Asmuth864311d2014-04-24 15:46:08 -04001275
1276 Presumes the Content-ID header conforms to the format that _id_to_header()
1277 returns.
1278
1279 Args:
1280 header: string, Content-ID header value.
1281
1282 Returns:
1283 The extracted id value.
1284
1285 Raises:
1286 BatchError if the header is not in the expected format.
1287 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001288 if header[0] != "<" or header[-1] != ">":
1289 raise BatchError("Invalid value for Content-ID: %s" % header)
1290 if "+" not in header:
1291 raise BatchError("Invalid value for Content-ID: %s" % header)
1292 base, id_ = header[1:-1].split(" + ", 1)
John Asmuth864311d2014-04-24 15:46:08 -04001293
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001294 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001295
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001296 def _serialize_request(self, request):
1297 """Convert an HttpRequest object into a string.
John Asmuth864311d2014-04-24 15:46:08 -04001298
1299 Args:
1300 request: HttpRequest, the request to serialize.
1301
1302 Returns:
1303 The request as a string in application/http format.
1304 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001305 # Construct status line
1306 parsed = urlparse(request.uri)
1307 request_line = urlunparse(
1308 ("", "", parsed.path, parsed.params, parsed.query, "")
John Asmuth864311d2014-04-24 15:46:08 -04001309 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001310 status_line = request.method + " " + request_line + " HTTP/1.1\n"
1311 major, minor = request.headers.get("content-type", "application/json").split(
1312 "/"
1313 )
1314 msg = MIMENonMultipart(major, minor)
1315 headers = request.headers.copy()
John Asmuth864311d2014-04-24 15:46:08 -04001316
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001317 if request.http is not None:
1318 credentials = _auth.get_credentials_from_http(request.http)
1319 if credentials is not None:
1320 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001321
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001322 # MIMENonMultipart adds its own Content-Type header.
1323 if "content-type" in headers:
1324 del headers["content-type"]
John Asmuth864311d2014-04-24 15:46:08 -04001325
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001326 for key, value in six.iteritems(headers):
1327 msg[key] = value
1328 msg["Host"] = parsed.netloc
1329 msg.set_unixfrom(None)
John Asmuth864311d2014-04-24 15:46:08 -04001330
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001331 if request.body is not None:
1332 msg.set_payload(request.body)
1333 msg["content-length"] = str(len(request.body))
John Asmuth864311d2014-04-24 15:46:08 -04001334
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001335 # Serialize the mime message.
1336 fp = StringIO()
1337 # maxheaderlen=0 means don't line wrap headers.
1338 g = Generator(fp, maxheaderlen=0)
1339 g.flatten(msg, unixfrom=False)
1340 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001341
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001342 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001344 def _deserialize_response(self, payload):
1345 """Convert string into httplib2 response and content.
John Asmuth864311d2014-04-24 15:46:08 -04001346
1347 Args:
1348 payload: string, headers and body as a string.
1349
1350 Returns:
1351 A pair (resp, content), such as would be returned from httplib2.request.
1352 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001353 # Strip off the status line
1354 status_line, payload = payload.split("\n", 1)
1355 protocol, status, reason = status_line.split(" ", 2)
John Asmuth864311d2014-04-24 15:46:08 -04001356
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001357 # Parse the rest of the response
1358 parser = FeedParser()
1359 parser.feed(payload)
1360 msg = parser.close()
1361 msg["status"] = status
John Asmuth864311d2014-04-24 15:46:08 -04001362
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001363 # Create httplib2.Response from the parsed headers.
1364 resp = httplib2.Response(msg)
1365 resp.reason = reason
1366 resp.version = int(protocol.split("/", 1)[1].replace(".", ""))
John Asmuth864311d2014-04-24 15:46:08 -04001367
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001368 content = payload.split("\r\n\r\n", 1)[1]
John Asmuth864311d2014-04-24 15:46:08 -04001369
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001370 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001371
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001372 def _new_id(self):
1373 """Create a new id.
John Asmuth864311d2014-04-24 15:46:08 -04001374
1375 Auto incrementing number that avoids conflicts with ids already used.
1376
1377 Returns:
1378 string, a new unique id.
1379 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001380 self._last_auto_id += 1
1381 while str(self._last_auto_id) in self._requests:
1382 self._last_auto_id += 1
1383 return str(self._last_auto_id)
John Asmuth864311d2014-04-24 15:46:08 -04001384
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001385 @util.positional(2)
1386 def add(self, request, callback=None, request_id=None):
1387 """Add a new request.
John Asmuth864311d2014-04-24 15:46:08 -04001388
1389 Every callback added will be paired with a unique id, the request_id. That
1390 unique id will be passed back to the callback when the response comes back
1391 from the server. The default behavior is to have the library generate it's
1392 own unique id. If the caller passes in a request_id then they must ensure
1393 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001394 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001395 request id, to avoid such an error.
1396
1397 Args:
1398 request: HttpRequest, Request to add to the batch.
1399 callback: callable, A callback to be called for this response, of the
1400 form callback(id, response, exception). The first parameter is the
1401 request id, and the second is the deserialized response object. The
1402 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1403 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001404 request_id: string, A unique id for the request. The id will be passed
1405 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001406
1407 Returns:
1408 None
1409
1410 Raises:
1411 BatchError if a media request is added to a batch.
1412 KeyError is the request_id is not unique.
1413 """
Xinan Line2dccec2018-12-07 05:28:33 +09001414
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001415 if len(self._order) >= MAX_BATCH_LIMIT:
1416 raise BatchError(
1417 "Exceeded the maximum calls(%d) in a single batch request."
1418 % MAX_BATCH_LIMIT
1419 )
1420 if request_id is None:
1421 request_id = self._new_id()
1422 if request.resumable is not None:
1423 raise BatchError("Media requests cannot be used in a batch request.")
1424 if request_id in self._requests:
1425 raise KeyError("A request with this ID already exists: %s" % request_id)
1426 self._requests[request_id] = request
1427 self._callbacks[request_id] = callback
1428 self._order.append(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001429
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001430 def _execute(self, http, order, requests):
1431 """Serialize batch request, send to server, process response.
John Asmuth864311d2014-04-24 15:46:08 -04001432
1433 Args:
1434 http: httplib2.Http, an http object to be used to make the request with.
1435 order: list, list of request ids in the order they were added to the
1436 batch.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001437 requests: list, list of request objects to send.
John Asmuth864311d2014-04-24 15:46:08 -04001438
1439 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001440 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001441 googleapiclient.errors.BatchError if the response is the wrong format.
1442 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001443 message = MIMEMultipart("mixed")
1444 # Message should not write out it's own headers.
1445 setattr(message, "_write_headers", lambda self: None)
John Asmuth864311d2014-04-24 15:46:08 -04001446
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001447 # Add all the individual requests.
1448 for request_id in order:
1449 request = requests[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001450
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001451 msg = MIMENonMultipart("application", "http")
1452 msg["Content-Transfer-Encoding"] = "binary"
1453 msg["Content-ID"] = self._id_to_header(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001454
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001455 body = self._serialize_request(request)
1456 msg.set_payload(body)
1457 message.attach(msg)
John Asmuth864311d2014-04-24 15:46:08 -04001458
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001459 # encode the body: note that we can't use `as_string`, because
1460 # it plays games with `From ` lines.
1461 fp = StringIO()
1462 g = Generator(fp, mangle_from_=False)
1463 g.flatten(message, unixfrom=False)
1464 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001465
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001466 headers = {}
1467 headers["content-type"] = (
1468 "multipart/mixed; " 'boundary="%s"'
1469 ) % message.get_boundary()
John Asmuth864311d2014-04-24 15:46:08 -04001470
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001471 resp, content = http.request(
1472 self._batch_uri, method="POST", body=body, headers=headers
1473 )
John Asmuth864311d2014-04-24 15:46:08 -04001474
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001475 if resp.status >= 300:
1476 raise HttpError(resp, content, uri=self._batch_uri)
John Asmuth864311d2014-04-24 15:46:08 -04001477
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001478 # Prepend with a content-type header so FeedParser can handle it.
1479 header = "content-type: %s\r\n\r\n" % resp["content-type"]
1480 # PY3's FeedParser only accepts unicode. So we should decode content
1481 # here, and encode each payload again.
1482 if six.PY3:
1483 content = content.decode("utf-8")
1484 for_parser = header + content
John Asmuth864311d2014-04-24 15:46:08 -04001485
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001486 parser = FeedParser()
1487 parser.feed(for_parser)
1488 mime_response = parser.close()
John Asmuth864311d2014-04-24 15:46:08 -04001489
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001490 if not mime_response.is_multipart():
1491 raise BatchError(
1492 "Response not in multipart/mixed format.", resp=resp, content=content
1493 )
John Asmuth864311d2014-04-24 15:46:08 -04001494
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001495 for part in mime_response.get_payload():
1496 request_id = self._header_to_id(part["Content-ID"])
1497 response, content = self._deserialize_response(part.get_payload())
1498 # We encode content here to emulate normal http response.
1499 if isinstance(content, six.text_type):
1500 content = content.encode("utf-8")
1501 self._responses[request_id] = (response, content)
John Asmuth864311d2014-04-24 15:46:08 -04001502
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001503 @util.positional(1)
1504 def execute(self, http=None):
1505 """Execute all the requests as a single batched HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001506
1507 Args:
1508 http: httplib2.Http, an http object to be used in place of the one the
1509 HttpRequest request object was constructed with. If one isn't supplied
1510 then use a http object from the requests in this batch.
1511
1512 Returns:
1513 None
1514
1515 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001516 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001517 googleapiclient.errors.BatchError if the response is the wrong format.
1518 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001519 # If we have no requests return
1520 if len(self._order) == 0:
1521 return None
John Asmuth864311d2014-04-24 15:46:08 -04001522
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001523 # If http is not supplied use the first valid one given in the requests.
1524 if http is None:
1525 for request_id in self._order:
1526 request = self._requests[request_id]
1527 if request is not None:
1528 http = request.http
1529 break
John Asmuth864311d2014-04-24 15:46:08 -04001530
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001531 if http is None:
1532 raise ValueError("Missing a valid http object.")
John Asmuth864311d2014-04-24 15:46:08 -04001533
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001534 # Special case for OAuth2Credentials-style objects which have not yet been
1535 # refreshed with an initial access_token.
1536 creds = _auth.get_credentials_from_http(http)
1537 if creds is not None:
1538 if not _auth.is_valid(creds):
1539 LOGGER.info("Attempting refresh to obtain initial access_token")
1540 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001541
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001542 self._execute(http, self._order, self._requests)
John Asmuth864311d2014-04-24 15:46:08 -04001543
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001544 # Loop over all the requests and check for 401s. For each 401 request the
1545 # credentials should be refreshed and then sent again in a separate batch.
1546 redo_requests = {}
1547 redo_order = []
John Asmuth864311d2014-04-24 15:46:08 -04001548
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001549 for request_id in self._order:
1550 resp, content = self._responses[request_id]
1551 if resp["status"] == "401":
1552 redo_order.append(request_id)
1553 request = self._requests[request_id]
1554 self._refresh_and_apply_credentials(request, http)
1555 redo_requests[request_id] = request
John Asmuth864311d2014-04-24 15:46:08 -04001556
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001557 if redo_requests:
1558 self._execute(http, redo_order, redo_requests)
John Asmuth864311d2014-04-24 15:46:08 -04001559
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001560 # Now process all callbacks that are erroring, and raise an exception for
1561 # ones that return a non-2xx response? Or add extra parameter to callback
1562 # that contains an HttpError?
John Asmuth864311d2014-04-24 15:46:08 -04001563
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001564 for request_id in self._order:
1565 resp, content = self._responses[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001566
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001567 request = self._requests[request_id]
1568 callback = self._callbacks[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001569
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001570 response = None
1571 exception = None
1572 try:
1573 if resp.status >= 300:
1574 raise HttpError(resp, content, uri=request.uri)
1575 response = request.postproc(resp, content)
1576 except HttpError as e:
1577 exception = e
John Asmuth864311d2014-04-24 15:46:08 -04001578
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001579 if callback is not None:
1580 callback(request_id, response, exception)
1581 if self._callback is not None:
1582 self._callback(request_id, response, exception)
John Asmuth864311d2014-04-24 15:46:08 -04001583
1584
1585class HttpRequestMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001586 """Mock of HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001587
1588 Do not construct directly, instead use RequestMockBuilder.
1589 """
1590
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001591 def __init__(self, resp, content, postproc):
1592 """Constructor for HttpRequestMock
John Asmuth864311d2014-04-24 15:46:08 -04001593
1594 Args:
1595 resp: httplib2.Response, the response to emulate coming from the request
1596 content: string, the response body
1597 postproc: callable, the post processing function usually supplied by
1598 the model class. See model.JsonModel.response() as an example.
1599 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001600 self.resp = resp
1601 self.content = content
1602 self.postproc = postproc
1603 if resp is None:
1604 self.resp = httplib2.Response({"status": 200, "reason": "OK"})
1605 if "reason" in self.resp:
1606 self.resp.reason = self.resp["reason"]
John Asmuth864311d2014-04-24 15:46:08 -04001607
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001608 def execute(self, http=None):
1609 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -04001610
1611 Same behavior as HttpRequest.execute(), but the response is
1612 mocked and not really from an HTTP request/response.
1613 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001614 return self.postproc(self.resp, self.content)
John Asmuth864311d2014-04-24 15:46:08 -04001615
1616
1617class RequestMockBuilder(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001618 """A simple mock of HttpRequest
John Asmuth864311d2014-04-24 15:46:08 -04001619
1620 Pass in a dictionary to the constructor that maps request methodIds to
1621 tuples of (httplib2.Response, content, opt_expected_body) that should be
1622 returned when that method is called. None may also be passed in for the
1623 httplib2.Response, in which case a 200 OK response will be generated.
1624 If an opt_expected_body (str or dict) is provided, it will be compared to
1625 the body and UnexpectedBodyError will be raised on inequality.
1626
1627 Example:
1628 response = '{"data": {"id": "tag:google.c...'
1629 requestBuilder = RequestMockBuilder(
1630 {
1631 'plus.activities.get': (None, response),
1632 }
1633 )
1634 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1635
1636 Methods that you do not supply a response for will return a
1637 200 OK with an empty string as the response content or raise an excpetion
1638 if check_unexpected is set to True. The methodId is taken from the rpcName
1639 in the discovery document.
1640
1641 For more details see the project wiki.
1642 """
1643
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001644 def __init__(self, responses, check_unexpected=False):
1645 """Constructor for RequestMockBuilder
John Asmuth864311d2014-04-24 15:46:08 -04001646
1647 The constructed object should be a callable object
1648 that can replace the class HttpResponse.
1649
1650 responses - A dictionary that maps methodIds into tuples
1651 of (httplib2.Response, content). The methodId
1652 comes from the 'rpcName' field in the discovery
1653 document.
1654 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1655 should be raised on unsupplied method.
1656 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001657 self.responses = responses
1658 self.check_unexpected = check_unexpected
John Asmuth864311d2014-04-24 15:46:08 -04001659
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001660 def __call__(
1661 self,
1662 http,
1663 postproc,
1664 uri,
1665 method="GET",
1666 body=None,
1667 headers=None,
1668 methodId=None,
1669 resumable=None,
1670 ):
1671 """Implements the callable interface that discovery.build() expects
John Asmuth864311d2014-04-24 15:46:08 -04001672 of requestBuilder, which is to build an object compatible with
1673 HttpRequest.execute(). See that method for the description of the
1674 parameters and the expected response.
1675 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001676 if methodId in self.responses:
1677 response = self.responses[methodId]
1678 resp, content = response[:2]
1679 if len(response) > 2:
1680 # Test the body against the supplied expected_body.
1681 expected_body = response[2]
1682 if bool(expected_body) != bool(body):
1683 # Not expecting a body and provided one
1684 # or expecting a body and not provided one.
1685 raise UnexpectedBodyError(expected_body, body)
1686 if isinstance(expected_body, str):
1687 expected_body = json.loads(expected_body)
1688 body = json.loads(body)
1689 if body != expected_body:
1690 raise UnexpectedBodyError(expected_body, body)
1691 return HttpRequestMock(resp, content, postproc)
1692 elif self.check_unexpected:
1693 raise UnexpectedMethodError(methodId=methodId)
1694 else:
1695 model = JsonModel(False)
1696 return HttpRequestMock(None, "{}", model.response)
John Asmuth864311d2014-04-24 15:46:08 -04001697
1698
1699class HttpMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001700 """Mock of httplib2.Http"""
John Asmuth864311d2014-04-24 15:46:08 -04001701
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001702 def __init__(self, filename=None, headers=None):
1703 """
John Asmuth864311d2014-04-24 15:46:08 -04001704 Args:
1705 filename: string, absolute filename to read response from
1706 headers: dict, header to return with response
1707 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001708 if headers is None:
1709 headers = {"status": "200"}
1710 if filename:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001711 with open(filename, "rb") as f:
1712 self.data = f.read()
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001713 else:
1714 self.data = None
1715 self.response_headers = headers
1716 self.headers = None
1717 self.uri = None
1718 self.method = None
1719 self.body = None
1720 self.headers = None
John Asmuth864311d2014-04-24 15:46:08 -04001721
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001722 def request(
1723 self,
1724 uri,
1725 method="GET",
1726 body=None,
1727 headers=None,
1728 redirections=1,
1729 connection_type=None,
1730 ):
1731 self.uri = uri
1732 self.method = method
1733 self.body = body
1734 self.headers = headers
1735 return httplib2.Response(self.response_headers), self.data
John Asmuth864311d2014-04-24 15:46:08 -04001736
Bu Sun Kim98888da2020-09-23 11:10:39 -06001737 def close(self):
1738 return None
John Asmuth864311d2014-04-24 15:46:08 -04001739
1740class HttpMockSequence(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001741 """Mock of httplib2.Http
John Asmuth864311d2014-04-24 15:46:08 -04001742
1743 Mocks a sequence of calls to request returning different responses for each
1744 call. Create an instance initialized with the desired response headers
1745 and content and then use as if an httplib2.Http instance.
1746
1747 http = HttpMockSequence([
1748 ({'status': '401'}, ''),
1749 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1750 ({'status': '200'}, 'echo_request_headers'),
1751 ])
1752 resp, content = http.request("http://examples.com")
1753
1754 There are special values you can pass in for content to trigger
1755 behavours that are helpful in testing.
1756
1757 'echo_request_headers' means return the request headers in the response body
1758 'echo_request_headers_as_json' means return the request headers in
1759 the response body
1760 'echo_request_body' means return the request body in the response body
1761 'echo_request_uri' means return the request uri in the response body
1762 """
1763
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001764 def __init__(self, iterable):
1765 """
John Asmuth864311d2014-04-24 15:46:08 -04001766 Args:
1767 iterable: iterable, a sequence of pairs of (headers, body)
1768 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001769 self._iterable = iterable
1770 self.follow_redirects = True
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001771 self.request_sequence = list()
John Asmuth864311d2014-04-24 15:46:08 -04001772
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001773 def request(
1774 self,
1775 uri,
1776 method="GET",
1777 body=None,
1778 headers=None,
1779 redirections=1,
1780 connection_type=None,
1781 ):
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001782 # Remember the request so after the fact this mock can be examined
1783 self.request_sequence.append((uri, method, body, headers))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001784 resp, content = self._iterable.pop(0)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001785 content = six.ensure_binary(content)
1786
1787 if content == b"echo_request_headers":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001788 content = headers
Matt McDonaldef6420a2020-04-14 16:28:13 -04001789 elif content == b"echo_request_headers_as_json":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001790 content = json.dumps(headers)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001791 elif content == b"echo_request_body":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001792 if hasattr(body, "read"):
1793 content = body.read()
1794 else:
1795 content = body
Matt McDonaldef6420a2020-04-14 16:28:13 -04001796 elif content == b"echo_request_uri":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001797 content = uri
1798 if isinstance(content, six.text_type):
1799 content = content.encode("utf-8")
1800 return httplib2.Response(resp), content
John Asmuth864311d2014-04-24 15:46:08 -04001801
1802
1803def set_user_agent(http, user_agent):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001804 """Set the user-agent on every request.
John Asmuth864311d2014-04-24 15:46:08 -04001805
1806 Args:
1807 http - An instance of httplib2.Http
1808 or something that acts like it.
1809 user_agent: string, the value for the user-agent header.
1810
1811 Returns:
1812 A modified instance of http that was passed in.
1813
1814 Example:
1815
1816 h = httplib2.Http()
1817 h = set_user_agent(h, "my-app-name/6.0")
1818
1819 Most of the time the user-agent will be set doing auth, this is for the rare
1820 cases where you are accessing an unauthenticated endpoint.
1821 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001822 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001823
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001824 # The closure that will replace 'httplib2.Http.request'.
1825 def new_request(
1826 uri,
1827 method="GET",
1828 body=None,
1829 headers=None,
1830 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1831 connection_type=None,
1832 ):
1833 """Modify the request headers to add the user-agent."""
1834 if headers is None:
1835 headers = {}
1836 if "user-agent" in headers:
1837 headers["user-agent"] = user_agent + " " + headers["user-agent"]
1838 else:
1839 headers["user-agent"] = user_agent
1840 resp, content = request_orig(
1841 uri,
1842 method=method,
1843 body=body,
1844 headers=headers,
1845 redirections=redirections,
1846 connection_type=connection_type,
1847 )
1848 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001849
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001850 http.request = new_request
1851 return http
John Asmuth864311d2014-04-24 15:46:08 -04001852
1853
1854def tunnel_patch(http):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001855 """Tunnel PATCH requests over POST.
John Asmuth864311d2014-04-24 15:46:08 -04001856 Args:
1857 http - An instance of httplib2.Http
1858 or something that acts like it.
1859
1860 Returns:
1861 A modified instance of http that was passed in.
1862
1863 Example:
1864
1865 h = httplib2.Http()
1866 h = tunnel_patch(h, "my-app-name/6.0")
1867
1868 Useful if you are running on a platform that doesn't support PATCH.
1869 Apply this last if you are using OAuth 1.0, as changing the method
1870 will result in a different signature.
1871 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001872 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001873
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001874 # The closure that will replace 'httplib2.Http.request'.
1875 def new_request(
1876 uri,
1877 method="GET",
1878 body=None,
1879 headers=None,
1880 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1881 connection_type=None,
1882 ):
1883 """Modify the request headers to add the user-agent."""
1884 if headers is None:
1885 headers = {}
1886 if method == "PATCH":
1887 if "oauth_token" in headers.get("authorization", ""):
1888 LOGGER.warning(
1889 "OAuth 1.0 request made with Credentials after tunnel_patch."
1890 )
1891 headers["x-http-method-override"] = "PATCH"
1892 method = "POST"
1893 resp, content = request_orig(
1894 uri,
1895 method=method,
1896 body=body,
1897 headers=headers,
1898 redirections=redirections,
1899 connection_type=connection_type,
1900 )
1901 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001902
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001903 http.request = new_request
1904 return http
Igor Maravić22435292017-01-19 22:28:22 +01001905
1906
1907def build_http():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001908 """Builds httplib2.Http object
Igor Maravić22435292017-01-19 22:28:22 +01001909
1910 Returns:
1911 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1912 To override default timeout call
1913
1914 socket.setdefaulttimeout(timeout_in_sec)
1915
1916 before interacting with this method.
1917 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001918 if socket.getdefaulttimeout() is not None:
1919 http_timeout = socket.getdefaulttimeout()
1920 else:
1921 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001922 http = httplib2.Http(timeout=http_timeout)
1923 # 308's are used by several Google APIs (Drive, YouTube)
1924 # for Resumable Uploads rather than Permanent Redirects.
1925 # This asks httplib2 to exclude 308s from the status codes
1926 # it treats as redirects
Bu Sun Kima480d532020-03-13 12:52:22 -07001927 try:
1928 http.redirect_codes = http.redirect_codes - {308}
1929 except AttributeError:
1930 # Apache Beam tests depend on this library and cannot
1931 # currently upgrade their httplib2 version
1932 # http.redirect_codes does not exist in previous versions
1933 # of httplib2, so pass
1934 pass
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001935
1936 return http