blob: 00467747fd3260f053810e6aab8ffd999c67e4fe [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):
119 reason = data["error"]["errors"][0]["reason"]
120 else:
121 reason = data[0]["error"]["errors"]["reason"]
122 except (UnicodeDecodeError, ValueError, KeyError):
123 LOGGER.warning("Invalid JSON content from response: %s", content)
124 return False
eesheeshc6425a02016-02-12 15:07:06 +0000125
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700126 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
eesheeshc6425a02016-02-12 15:07:06 +0000127
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700128 # Only retry on rate limit related failures.
129 if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
130 return True
eesheeshc6425a02016-02-12 15:07:06 +0000131
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700132 # Everything else is a success or non-retriable so break.
133 return False
eesheeshc6425a02016-02-12 15:07:06 +0000134
John Asmuth864311d2014-04-24 15:46:08 -0400135
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700136def _retry_request(
137 http, num_retries, req_type, sleep, rand, uri, method, *args, **kwargs
138):
139 """Retries an HTTP request multiple times while handling errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100140
141 If after all retries the request still fails, last error is either returned as
142 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
143
144 Args:
145 http: Http object to be used to execute request.
146 num_retries: Maximum number of retries.
147 req_type: Type of the request (used for logging retries).
148 sleep, rand: Functions to sleep for random time between retries.
149 uri: URI to be requested.
150 method: HTTP method to be used.
151 args, kwargs: Additional arguments passed to http.request.
152
153 Returns:
154 resp, content - Response from the http request (may be HTTP 5xx).
155 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700156 resp = None
157 content = None
158 exception = None
159 for retry_num in range(num_retries + 1):
160 if retry_num > 0:
161 # Sleep before retrying.
162 sleep_time = rand() * 2 ** retry_num
163 LOGGER.warning(
164 "Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s",
165 sleep_time,
166 retry_num,
167 num_retries,
168 req_type,
169 method,
170 uri,
171 resp.status if resp else exception,
172 )
173 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100174
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700175 try:
176 exception = None
177 resp, content = http.request(uri, method, *args, **kwargs)
178 # Retry on SSL errors and socket timeout errors.
179 except _ssl_SSLError as ssl_error:
180 exception = ssl_error
181 except socket.timeout as socket_timeout:
182 # It's important that this be before socket.error as it's a subclass
183 # socket.timeout has no errorcode
184 exception = socket_timeout
Damian Gadomskic7516a22020-03-23 20:39:21 +0100185 except ConnectionError as connection_error:
186 # Needs to be before socket.error as it's a subclass of
187 # OSError (socket.error)
188 exception = connection_error
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700189 except socket.error as socket_error:
190 # errno's contents differ by platform, so we have to match by name.
191 if socket.errno.errorcode.get(socket_error.errno) not in {
192 "WSAETIMEDOUT",
193 "ETIMEDOUT",
194 "EPIPE",
195 "ECONNABORTED",
196 }:
197 raise
198 exception = socket_error
199 except httplib2.ServerNotFoundError as server_not_found_error:
200 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000201
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700202 if exception:
203 if retry_num == num_retries:
204 raise exception
205 else:
206 continue
eesheeshc6425a02016-02-12 15:07:06 +0000207
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700208 if not _should_retry_response(resp.status, content):
209 break
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100210
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700211 return resp, content
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100212
213
John Asmuth864311d2014-04-24 15:46:08 -0400214class MediaUploadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700215 """Status of a resumable upload."""
John Asmuth864311d2014-04-24 15:46:08 -0400216
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700217 def __init__(self, resumable_progress, total_size):
218 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400219
220 Args:
221 resumable_progress: int, bytes sent so far.
222 total_size: int, total bytes in complete upload, or None if the total
223 upload size isn't known ahead of time.
224 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700225 self.resumable_progress = resumable_progress
226 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400227
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700228 def progress(self):
229 """Percent of upload completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400230
231 Returns:
232 the percentage complete as a float, returning 0.0 if the total size of
233 the upload is unknown.
234 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700235 if self.total_size is not None and self.total_size != 0:
236 return float(self.resumable_progress) / float(self.total_size)
237 else:
238 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400239
240
241class MediaDownloadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700242 """Status of a resumable download."""
John Asmuth864311d2014-04-24 15:46:08 -0400243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700244 def __init__(self, resumable_progress, total_size):
245 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400246
247 Args:
248 resumable_progress: int, bytes received so far.
249 total_size: int, total bytes in complete download.
250 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700251 self.resumable_progress = resumable_progress
252 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400253
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700254 def progress(self):
255 """Percent of download completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400256
257 Returns:
258 the percentage complete as a float, returning 0.0 if the total size of
259 the download is unknown.
260 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700261 if self.total_size is not None and self.total_size != 0:
262 return float(self.resumable_progress) / float(self.total_size)
263 else:
264 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400265
266
267class MediaUpload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700268 """Describes a media object to upload.
John Asmuth864311d2014-04-24 15:46:08 -0400269
270 Base class that defines the interface of MediaUpload subclasses.
271
272 Note that subclasses of MediaUpload may allow you to control the chunksize
273 when uploading a media object. It is important to keep the size of the chunk
274 as large as possible to keep the upload efficient. Other factors may influence
275 the size of the chunk you use, particularly if you are working in an
276 environment where individual HTTP requests may have a hardcoded time limit,
277 such as under certain classes of requests under Google App Engine.
278
279 Streams are io.Base compatible objects that support seek(). Some MediaUpload
280 subclasses support using streams directly to upload data. Support for
281 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
282 platform that stream will be used for uploading the media object. The support
283 for streaming is indicated by has_stream() returning True. The stream() method
284 should return an io.Base object that supports seek(). On platforms where the
285 underlying httplib module supports streaming, for example Python 2.6 and
286 later, the stream will be passed into the http library which will result in
287 less memory being used and possibly faster uploads.
288
289 If you need to upload media that can't be uploaded using any of the existing
290 MediaUpload sub-class then you can sub-class MediaUpload for your particular
291 needs.
292 """
293
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700294 def chunksize(self):
295 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400296
297 Returns:
298 Chunk size in bytes.
299 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700300 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400301
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700302 def mimetype(self):
303 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400304
305 Returns:
306 Mime type.
307 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700308 return "application/octet-stream"
John Asmuth864311d2014-04-24 15:46:08 -0400309
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700310 def size(self):
311 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400312
313 Returns:
314 Size of the body, or None of the size is unknown.
315 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700316 return None
John Asmuth864311d2014-04-24 15:46:08 -0400317
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700318 def resumable(self):
319 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400320
321 Returns:
322 True if resumable upload or False.
323 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700324 return False
John Asmuth864311d2014-04-24 15:46:08 -0400325
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700326 def getbytes(self, begin, end):
327 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400328
329 Args:
330 begin: int, offset from beginning of file.
331 length: int, number of bytes to read, starting at begin.
332
333 Returns:
334 A string of bytes read. May be shorter than length if EOF was reached
335 first.
336 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700337 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400338
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700339 def has_stream(self):
340 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400341
342 Streaming means it is an io.IOBase subclass that supports seek, i.e.
343 seekable() returns True.
344
345 Returns:
346 True if the call to stream() will return an instance of a seekable io.Base
347 subclass.
348 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700349 return False
John Asmuth864311d2014-04-24 15:46:08 -0400350
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700351 def stream(self):
352 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400353
354 Returns:
355 The returned value is an io.IOBase subclass that supports seek, i.e.
356 seekable() returns True.
357 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700358 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400359
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700360 @util.positional(1)
361 def _to_json(self, strip=None):
362 """Utility function for creating a JSON representation of a MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400363
364 Args:
365 strip: array, An array of names of members to not include in the JSON.
366
367 Returns:
368 string, a JSON representation of this instance, suitable to pass to
369 from_json().
370 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700371 t = type(self)
372 d = copy.copy(self.__dict__)
373 if strip is not None:
374 for member in strip:
375 del d[member]
376 d["_class"] = t.__name__
377 d["_module"] = t.__module__
378 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400379
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700380 def to_json(self):
381 """Create a JSON representation of an instance of MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400382
383 Returns:
384 string, a JSON representation of this instance, suitable to pass to
385 from_json().
386 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700387 return self._to_json()
John Asmuth864311d2014-04-24 15:46:08 -0400388
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700389 @classmethod
390 def new_from_json(cls, s):
391 """Utility class method to instantiate a MediaUpload subclass from a JSON
John Asmuth864311d2014-04-24 15:46:08 -0400392 representation produced by to_json().
393
394 Args:
395 s: string, JSON from to_json().
396
397 Returns:
398 An instance of the subclass of MediaUpload that was serialized with
399 to_json().
400 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700401 data = json.loads(s)
402 # Find and call the right classmethod from_json() to restore the object.
403 module = data["_module"]
404 m = __import__(module, fromlist=module.split(".")[:-1])
405 kls = getattr(m, data["_class"])
406 from_json = getattr(kls, "from_json")
407 return from_json(s)
John Asmuth864311d2014-04-24 15:46:08 -0400408
409
410class MediaIoBaseUpload(MediaUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700411 """A MediaUpload for a io.Base objects.
John Asmuth864311d2014-04-24 15:46:08 -0400412
413 Note that the Python file object is compatible with io.Base and can be used
414 with this class also.
415
Pat Ferateed9affd2015-03-03 16:03:15 -0800416 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400417 media = MediaIoBaseUpload(fh, mimetype='image/png',
418 chunksize=1024*1024, resumable=True)
419 farm.animals().insert(
420 id='cow',
421 name='cow.png',
422 media_body=media).execute()
423
424 Depending on the platform you are working on, you may pass -1 as the
425 chunksize, which indicates that the entire file should be uploaded in a single
426 request. If the underlying platform supports streams, such as Python 2.6 or
427 later, then this can be very efficient as it avoids multiple connections, and
428 also avoids loading the entire file into memory before sending it. Note that
429 Google App Engine has a 5MB limit on request size, so you should never set
430 your chunksize larger than 5MB, or to -1.
431 """
432
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700433 @util.positional(3)
434 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
435 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400436
437 Args:
438 fd: io.Base or file object, The source of the bytes to upload. MUST be
439 opened in blocking mode, do not use streams opened in non-blocking mode.
440 The given stream must be seekable, that is, it must be able to call
441 seek() on fd.
442 mimetype: string, Mime-type of the file.
443 chunksize: int, File will be uploaded in chunks of this many bytes. Only
444 used if resumable=True. Pass in a value of -1 if the file is to be
445 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
446 on request size, so you should never set your chunksize larger than 5MB,
447 or to -1.
448 resumable: bool, True if this is a resumable upload. False means upload
449 in a single request.
450 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700451 super(MediaIoBaseUpload, self).__init__()
452 self._fd = fd
453 self._mimetype = mimetype
454 if not (chunksize == -1 or chunksize > 0):
455 raise InvalidChunkSizeError()
456 self._chunksize = chunksize
457 self._resumable = resumable
John Asmuth864311d2014-04-24 15:46:08 -0400458
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700459 self._fd.seek(0, os.SEEK_END)
460 self._size = self._fd.tell()
John Asmuth864311d2014-04-24 15:46:08 -0400461
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700462 def chunksize(self):
463 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400464
465 Returns:
466 Chunk size in bytes.
467 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700468 return self._chunksize
John Asmuth864311d2014-04-24 15:46:08 -0400469
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700470 def mimetype(self):
471 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400472
473 Returns:
474 Mime type.
475 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700476 return self._mimetype
John Asmuth864311d2014-04-24 15:46:08 -0400477
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700478 def size(self):
479 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400480
481 Returns:
482 Size of the body, or None of the size is unknown.
483 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700484 return self._size
John Asmuth864311d2014-04-24 15:46:08 -0400485
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700486 def resumable(self):
487 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400488
489 Returns:
490 True if resumable upload or False.
491 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700492 return self._resumable
John Asmuth864311d2014-04-24 15:46:08 -0400493
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700494 def getbytes(self, begin, length):
495 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400496
497 Args:
498 begin: int, offset from beginning of file.
499 length: int, number of bytes to read, starting at begin.
500
501 Returns:
502 A string of bytes read. May be shorted than length if EOF was reached
503 first.
504 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700505 self._fd.seek(begin)
506 return self._fd.read(length)
John Asmuth864311d2014-04-24 15:46:08 -0400507
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700508 def has_stream(self):
509 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400510
511 Streaming means it is an io.IOBase subclass that supports seek, i.e.
512 seekable() returns True.
513
514 Returns:
515 True if the call to stream() will return an instance of a seekable io.Base
516 subclass.
517 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700518 return True
John Asmuth864311d2014-04-24 15:46:08 -0400519
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700520 def stream(self):
521 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400522
523 Returns:
524 The returned value is an io.IOBase subclass that supports seek, i.e.
525 seekable() returns True.
526 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700527 return self._fd
John Asmuth864311d2014-04-24 15:46:08 -0400528
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700529 def to_json(self):
530 """This upload type is not serializable."""
531 raise NotImplementedError("MediaIoBaseUpload is not serializable.")
John Asmuth864311d2014-04-24 15:46:08 -0400532
533
534class MediaFileUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700535 """A MediaUpload for a file.
John Asmuth864311d2014-04-24 15:46:08 -0400536
537 Construct a MediaFileUpload and pass as the media_body parameter of the
538 method. For example, if we had a service that allowed uploading images:
539
John Asmuth864311d2014-04-24 15:46:08 -0400540 media = MediaFileUpload('cow.png', mimetype='image/png',
541 chunksize=1024*1024, resumable=True)
542 farm.animals().insert(
543 id='cow',
544 name='cow.png',
545 media_body=media).execute()
546
547 Depending on the platform you are working on, you may pass -1 as the
548 chunksize, which indicates that the entire file should be uploaded in a single
549 request. If the underlying platform supports streams, such as Python 2.6 or
550 later, then this can be very efficient as it avoids multiple connections, and
551 also avoids loading the entire file into memory before sending it. Note that
552 Google App Engine has a 5MB limit on request size, so you should never set
553 your chunksize larger than 5MB, or to -1.
554 """
555
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700556 @util.positional(2)
557 def __init__(
558 self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False
559 ):
560 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400561
562 Args:
563 filename: string, Name of the file.
564 mimetype: string, Mime-type of the file. If None then a mime-type will be
565 guessed from the file extension.
566 chunksize: int, File will be uploaded in chunks of this many bytes. Only
567 used if resumable=True. Pass in a value of -1 if the file is to be
568 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
569 on request size, so you should never set your chunksize larger than 5MB,
570 or to -1.
571 resumable: bool, True if this is a resumable upload. False means upload
572 in a single request.
573 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700574 self._filename = filename
575 fd = open(self._filename, "rb")
576 if mimetype is None:
577 # No mimetype provided, make a guess.
578 mimetype, _ = mimetypes.guess_type(filename)
579 if mimetype is None:
580 # Guess failed, use octet-stream.
581 mimetype = "application/octet-stream"
582 super(MediaFileUpload, self).__init__(
583 fd, mimetype, chunksize=chunksize, resumable=resumable
584 )
John Asmuth864311d2014-04-24 15:46:08 -0400585
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700586 def __del__(self):
587 self._fd.close()
Xiaofei Wang20b67582019-07-17 11:16:53 -0700588
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700589 def to_json(self):
590 """Creating a JSON representation of an instance of MediaFileUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400591
592 Returns:
593 string, a JSON representation of this instance, suitable to pass to
594 from_json().
595 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700596 return self._to_json(strip=["_fd"])
John Asmuth864311d2014-04-24 15:46:08 -0400597
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700598 @staticmethod
599 def from_json(s):
600 d = json.loads(s)
601 return MediaFileUpload(
602 d["_filename"],
603 mimetype=d["_mimetype"],
604 chunksize=d["_chunksize"],
605 resumable=d["_resumable"],
606 )
John Asmuth864311d2014-04-24 15:46:08 -0400607
608
609class MediaInMemoryUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700610 """MediaUpload for a chunk of bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400611
612 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
613 the stream.
614 """
615
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700616 @util.positional(2)
617 def __init__(
618 self,
619 body,
620 mimetype="application/octet-stream",
621 chunksize=DEFAULT_CHUNK_SIZE,
622 resumable=False,
623 ):
624 """Create a new MediaInMemoryUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400625
626 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
627 the stream.
628
629 Args:
630 body: string, Bytes of body content.
631 mimetype: string, Mime-type of the file or default of
632 'application/octet-stream'.
633 chunksize: int, File will be uploaded in chunks of this many bytes. Only
634 used if resumable=True.
635 resumable: bool, True if this is a resumable upload. False means upload
636 in a single request.
637 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700638 fd = BytesIO(body)
639 super(MediaInMemoryUpload, self).__init__(
640 fd, mimetype, chunksize=chunksize, resumable=resumable
641 )
John Asmuth864311d2014-04-24 15:46:08 -0400642
643
644class MediaIoBaseDownload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700645 """"Download media resources.
John Asmuth864311d2014-04-24 15:46:08 -0400646
647 Note that the Python file object is compatible with io.Base and can be used
648 with this class also.
649
650
651 Example:
652 request = farms.animals().get_media(id='cow')
653 fh = io.FileIO('cow.png', mode='wb')
654 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
655
656 done = False
657 while done is False:
658 status, done = downloader.next_chunk()
659 if status:
660 print "Download %d%%." % int(status.progress() * 100)
661 print "Download Complete!"
662 """
663
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700664 @util.positional(3)
665 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
666 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400667
668 Args:
669 fd: io.Base or file object, The stream in which to write the downloaded
670 bytes.
671 request: googleapiclient.http.HttpRequest, the media request to perform in
672 chunks.
673 chunksize: int, File will be downloaded in chunks of this many bytes.
674 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700675 self._fd = fd
676 self._request = request
677 self._uri = request.uri
678 self._chunksize = chunksize
679 self._progress = 0
680 self._total_size = None
681 self._done = False
John Asmuth864311d2014-04-24 15:46:08 -0400682
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700683 # Stubs for testing.
684 self._sleep = time.sleep
685 self._rand = random.random
John Asmuth864311d2014-04-24 15:46:08 -0400686
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700687 self._headers = {}
688 for k, v in six.iteritems(request.headers):
689 # allow users to supply custom headers by setting them on the request
690 # but strip out the ones that are set by default on requests generated by
691 # API methods like Drive's files().get(fileId=...)
692 if not k.lower() in ("accept", "accept-encoding", "user-agent"):
693 self._headers[k] = v
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400694
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700695 @util.positional(1)
696 def next_chunk(self, num_retries=0):
697 """Get the next chunk of the download.
John Asmuth864311d2014-04-24 15:46:08 -0400698
699 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500700 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400701 exponential backoff. If all retries fail, the raised HttpError
702 represents the last request. If zero (default), we attempt the
703 request only once.
704
705 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700706 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400707 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100708 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400709
710 Raises:
711 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000712 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400713 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700714 headers = self._headers.copy()
715 headers["range"] = "bytes=%d-%d" % (
716 self._progress,
717 self._progress + self._chunksize,
718 )
719 http = self._request.http
John Asmuth864311d2014-04-24 15:46:08 -0400720
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700721 resp, content = _retry_request(
722 http,
723 num_retries,
724 "media download",
725 self._sleep,
726 self._rand,
727 self._uri,
728 "GET",
729 headers=headers,
730 )
John Asmuth864311d2014-04-24 15:46:08 -0400731
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700732 if resp.status in [200, 206]:
733 if "content-location" in resp and resp["content-location"] != self._uri:
734 self._uri = resp["content-location"]
735 self._progress += len(content)
736 self._fd.write(content)
John Asmuth864311d2014-04-24 15:46:08 -0400737
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700738 if "content-range" in resp:
739 content_range = resp["content-range"]
740 length = content_range.rsplit("/", 1)[1]
741 self._total_size = int(length)
742 elif "content-length" in resp:
743 self._total_size = int(resp["content-length"])
John Asmuth864311d2014-04-24 15:46:08 -0400744
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700745 if self._total_size is None or self._progress == self._total_size:
746 self._done = True
747 return MediaDownloadProgress(self._progress, self._total_size), self._done
Bu Sun Kim86d87882020-10-22 08:51:16 -0600748 elif resp.status == 416:
749 # 416 is Range Not Satisfiable
750 # This typically occurs with a zero byte file
751 content_range = resp["content-range"]
752 length = content_range.rsplit("/", 1)[1]
753 self._total_size = int(length)
754 if self._total_size == 0:
755 self._done = True
756 return MediaDownloadProgress(self._progress, self._total_size), self._done
757 raise HttpError(resp, content, uri=self._uri)
John Asmuth864311d2014-04-24 15:46:08 -0400758
759
760class _StreamSlice(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700761 """Truncated stream.
John Asmuth864311d2014-04-24 15:46:08 -0400762
763 Takes a stream and presents a stream that is a slice of the original stream.
764 This is used when uploading media in chunks. In later versions of Python a
765 stream can be passed to httplib in place of the string of data to send. The
766 problem is that httplib just blindly reads to the end of the stream. This
767 wrapper presents a virtual stream that only reads to the end of the chunk.
768 """
769
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700770 def __init__(self, stream, begin, chunksize):
771 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400772
773 Args:
774 stream: (io.Base, file object), the stream to wrap.
775 begin: int, the seek position the chunk begins at.
776 chunksize: int, the size of the chunk.
777 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700778 self._stream = stream
779 self._begin = begin
780 self._chunksize = chunksize
781 self._stream.seek(begin)
John Asmuth864311d2014-04-24 15:46:08 -0400782
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700783 def read(self, n=-1):
784 """Read n bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400785
786 Args:
787 n, int, the number of bytes to read.
788
789 Returns:
790 A string of length 'n', or less if EOF is reached.
791 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700792 # The data left available to read sits in [cur, end)
793 cur = self._stream.tell()
794 end = self._begin + self._chunksize
795 if n == -1 or cur + n > end:
796 n = end - cur
797 return self._stream.read(n)
John Asmuth864311d2014-04-24 15:46:08 -0400798
799
800class HttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700801 """Encapsulates a single HTTP request."""
John Asmuth864311d2014-04-24 15:46:08 -0400802
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700803 @util.positional(4)
804 def __init__(
805 self,
806 http,
807 postproc,
808 uri,
809 method="GET",
810 body=None,
811 headers=None,
812 methodId=None,
813 resumable=None,
814 ):
815 """Constructor for an HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -0400816
817 Args:
818 http: httplib2.Http, the transport object to use to make a request
819 postproc: callable, called on the HTTP response and content to transform
820 it into a data object before returning, or raising an exception
821 on an error.
822 uri: string, the absolute URI to send the request to
823 method: string, the HTTP method to use
824 body: string, the request body of the HTTP request,
825 headers: dict, the HTTP request headers
826 methodId: string, a unique identifier for the API method being called.
827 resumable: MediaUpload, None if this is not a resumbale request.
828 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700829 self.uri = uri
830 self.method = method
831 self.body = body
832 self.headers = headers or {}
833 self.methodId = methodId
834 self.http = http
835 self.postproc = postproc
836 self.resumable = resumable
837 self.response_callbacks = []
838 self._in_error_state = False
John Asmuth864311d2014-04-24 15:46:08 -0400839
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700840 # The size of the non-media part of the request.
841 self.body_size = len(self.body or "")
John Asmuth864311d2014-04-24 15:46:08 -0400842
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700843 # The resumable URI to send chunks to.
844 self.resumable_uri = None
John Asmuth864311d2014-04-24 15:46:08 -0400845
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700846 # The bytes that have been uploaded.
847 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -0400848
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700849 # Stubs for testing.
850 self._rand = random.random
851 self._sleep = time.sleep
John Asmuth864311d2014-04-24 15:46:08 -0400852
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700853 @util.positional(1)
854 def execute(self, http=None, num_retries=0):
855 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -0400856
857 Args:
858 http: httplib2.Http, an http object to be used in place of the
859 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500860 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400861 exponential backoff. If all retries fail, the raised HttpError
862 represents the last request. If zero (default), we attempt the
863 request only once.
864
865 Returns:
866 A deserialized object model of the response body as determined
867 by the postproc.
868
869 Raises:
870 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000871 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400872 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700873 if http is None:
874 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400875
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700876 if self.resumable:
877 body = None
878 while body is None:
879 _, body = self.next_chunk(http=http, num_retries=num_retries)
880 return body
John Asmuth864311d2014-04-24 15:46:08 -0400881
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700882 # Non-resumable case.
John Asmuth864311d2014-04-24 15:46:08 -0400883
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700884 if "content-length" not in self.headers:
885 self.headers["content-length"] = str(self.body_size)
886 # If the request URI is too long then turn it into a POST request.
887 # Assume that a GET request never contains a request body.
888 if len(self.uri) > MAX_URI_LENGTH and self.method == "GET":
889 self.method = "POST"
890 self.headers["x-http-method-override"] = "GET"
891 self.headers["content-type"] = "application/x-www-form-urlencoded"
892 parsed = urlparse(self.uri)
893 self.uri = urlunparse(
894 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
895 )
896 self.body = parsed.query
897 self.headers["content-length"] = str(len(self.body))
John Asmuth864311d2014-04-24 15:46:08 -0400898
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700899 # Handle retries for server-side errors.
900 resp, content = _retry_request(
901 http,
902 num_retries,
903 "request",
904 self._sleep,
905 self._rand,
906 str(self.uri),
907 method=str(self.method),
908 body=self.body,
909 headers=self.headers,
910 )
John Asmuth864311d2014-04-24 15:46:08 -0400911
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700912 for callback in self.response_callbacks:
913 callback(resp)
914 if resp.status >= 300:
915 raise HttpError(resp, content, uri=self.uri)
916 return self.postproc(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400917
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700918 @util.positional(2)
919 def add_response_callback(self, cb):
920 """add_response_headers_callback
John Asmuth864311d2014-04-24 15:46:08 -0400921
922 Args:
923 cb: Callback to be called on receiving the response headers, of signature:
924
925 def cb(resp):
926 # Where resp is an instance of httplib2.Response
927 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700928 self.response_callbacks.append(cb)
John Asmuth864311d2014-04-24 15:46:08 -0400929
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700930 @util.positional(1)
931 def next_chunk(self, http=None, num_retries=0):
932 """Execute the next step of a resumable upload.
John Asmuth864311d2014-04-24 15:46:08 -0400933
934 Can only be used if the method being executed supports media uploads and
935 the MediaUpload object passed in was flagged as using resumable upload.
936
937 Example:
938
939 media = MediaFileUpload('cow.png', mimetype='image/png',
940 chunksize=1000, resumable=True)
941 request = farm.animals().insert(
942 id='cow',
943 name='cow.png',
944 media_body=media)
945
946 response = None
947 while response is None:
948 status, response = request.next_chunk()
949 if status:
950 print "Upload %d%% complete." % int(status.progress() * 100)
951
952
953 Args:
954 http: httplib2.Http, an http object to be used in place of the
955 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500956 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400957 exponential backoff. If all retries fail, the raised HttpError
958 represents the last request. If zero (default), we attempt the
959 request only once.
960
961 Returns:
962 (status, body): (ResumableMediaStatus, object)
963 The body will be None until the resumable media is fully uploaded.
964
965 Raises:
966 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000967 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400968 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700969 if http is None:
970 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400971
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700972 if self.resumable.size() is None:
973 size = "*"
974 else:
975 size = str(self.resumable.size())
John Asmuth864311d2014-04-24 15:46:08 -0400976
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700977 if self.resumable_uri is None:
978 start_headers = copy.copy(self.headers)
979 start_headers["X-Upload-Content-Type"] = self.resumable.mimetype()
980 if size != "*":
981 start_headers["X-Upload-Content-Length"] = size
982 start_headers["content-length"] = str(self.body_size)
John Asmuth864311d2014-04-24 15:46:08 -0400983
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700984 resp, content = _retry_request(
985 http,
986 num_retries,
987 "resumable URI request",
988 self._sleep,
989 self._rand,
990 self.uri,
991 method=self.method,
992 body=self.body,
993 headers=start_headers,
994 )
John Asmuth864311d2014-04-24 15:46:08 -0400995
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700996 if resp.status == 200 and "location" in resp:
997 self.resumable_uri = resp["location"]
998 else:
999 raise ResumableUploadError(resp, content)
1000 elif self._in_error_state:
1001 # If we are in an error state then query the server for current state of
1002 # the upload by sending an empty PUT and reading the 'range' header in
1003 # the response.
1004 headers = {"Content-Range": "bytes */%s" % size, "content-length": "0"}
1005 resp, content = http.request(self.resumable_uri, "PUT", headers=headers)
1006 status, body = self._process_response(resp, content)
1007 if body:
1008 # The upload was complete.
1009 return (status, body)
John Asmuth864311d2014-04-24 15:46:08 -04001010
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001011 if self.resumable.has_stream():
1012 data = self.resumable.stream()
1013 if self.resumable.chunksize() == -1:
1014 data.seek(self.resumable_progress)
1015 chunk_end = self.resumable.size() - self.resumable_progress - 1
1016 else:
1017 # Doing chunking with a stream, so wrap a slice of the stream.
1018 data = _StreamSlice(
1019 data, self.resumable_progress, self.resumable.chunksize()
1020 )
1021 chunk_end = min(
1022 self.resumable_progress + self.resumable.chunksize() - 1,
1023 self.resumable.size() - 1,
1024 )
1025 else:
1026 data = self.resumable.getbytes(
1027 self.resumable_progress, self.resumable.chunksize()
1028 )
John Asmuth864311d2014-04-24 15:46:08 -04001029
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001030 # A short read implies that we are at EOF, so finish the upload.
1031 if len(data) < self.resumable.chunksize():
1032 size = str(self.resumable_progress + len(data))
John Asmuth864311d2014-04-24 15:46:08 -04001033
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001034 chunk_end = self.resumable_progress + len(data) - 1
John Asmuth864311d2014-04-24 15:46:08 -04001035
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001036 headers = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001037 # Must set the content-length header here because httplib can't
1038 # calculate the size when working with _StreamSlice.
1039 "Content-Length": str(chunk_end - self.resumable_progress + 1),
John Asmuth864311d2014-04-24 15:46:08 -04001040 }
1041
Bu Sun Kimaf6035f2020-10-20 16:36:04 -06001042 # An empty file results in chunk_end = -1 and size = 0
1043 # sending "bytes 0--1/0" results in an invalid request
1044 # Only add header "Content-Range" if chunk_end != -1
1045 if chunk_end != -1:
1046 headers["Content-Range"] = "bytes %d-%d/%s" % (self.resumable_progress, chunk_end, size)
1047
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001048 for retry_num in range(num_retries + 1):
1049 if retry_num > 0:
1050 self._sleep(self._rand() * 2 ** retry_num)
1051 LOGGER.warning(
1052 "Retry #%d for media upload: %s %s, following status: %d"
1053 % (retry_num, self.method, self.uri, resp.status)
1054 )
John Asmuth864311d2014-04-24 15:46:08 -04001055
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001056 try:
1057 resp, content = http.request(
1058 self.resumable_uri, method="PUT", body=data, headers=headers
1059 )
1060 except:
1061 self._in_error_state = True
1062 raise
1063 if not _should_retry_response(resp.status, content):
1064 break
John Asmuth864311d2014-04-24 15:46:08 -04001065
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001066 return self._process_response(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -04001067
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001068 def _process_response(self, resp, content):
1069 """Process the response from a single chunk upload.
John Asmuth864311d2014-04-24 15:46:08 -04001070
1071 Args:
1072 resp: httplib2.Response, the response object.
1073 content: string, the content of the response.
1074
1075 Returns:
1076 (status, body): (ResumableMediaStatus, object)
1077 The body will be None until the resumable media is fully uploaded.
1078
1079 Raises:
1080 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1081 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001082 if resp.status in [200, 201]:
1083 self._in_error_state = False
1084 return None, self.postproc(resp, content)
1085 elif resp.status == 308:
1086 self._in_error_state = False
1087 # A "308 Resume Incomplete" indicates we are not done.
1088 try:
1089 self.resumable_progress = int(resp["range"].split("-")[1]) + 1
1090 except KeyError:
1091 # If resp doesn't contain range header, resumable progress is 0
1092 self.resumable_progress = 0
1093 if "location" in resp:
1094 self.resumable_uri = resp["location"]
1095 else:
1096 self._in_error_state = True
1097 raise HttpError(resp, content, uri=self.uri)
John Asmuth864311d2014-04-24 15:46:08 -04001098
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001099 return (
1100 MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1101 None,
1102 )
John Asmuth864311d2014-04-24 15:46:08 -04001103
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001104 def to_json(self):
1105 """Returns a JSON representation of the HttpRequest."""
1106 d = copy.copy(self.__dict__)
1107 if d["resumable"] is not None:
1108 d["resumable"] = self.resumable.to_json()
1109 del d["http"]
1110 del d["postproc"]
1111 del d["_sleep"]
1112 del d["_rand"]
John Asmuth864311d2014-04-24 15:46:08 -04001113
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001114 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001115
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001116 @staticmethod
1117 def from_json(s, http, postproc):
1118 """Returns an HttpRequest populated with info from a JSON object."""
1119 d = json.loads(s)
1120 if d["resumable"] is not None:
1121 d["resumable"] = MediaUpload.new_from_json(d["resumable"])
1122 return HttpRequest(
1123 http,
1124 postproc,
1125 uri=d["uri"],
1126 method=d["method"],
1127 body=d["body"],
1128 headers=d["headers"],
1129 methodId=d["methodId"],
1130 resumable=d["resumable"],
1131 )
John Asmuth864311d2014-04-24 15:46:08 -04001132
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001133 @staticmethod
1134 def null_postproc(resp, contents):
1135 return resp, contents
1136
John Asmuth864311d2014-04-24 15:46:08 -04001137
1138class BatchHttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001139 """Batches multiple HttpRequest objects into a single HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001140
1141 Example:
1142 from googleapiclient.http import BatchHttpRequest
1143
1144 def list_animals(request_id, response, exception):
1145 \"\"\"Do something with the animals list response.\"\"\"
1146 if exception is not None:
1147 # Do something with the exception.
1148 pass
1149 else:
1150 # Do something with the response.
1151 pass
1152
1153 def list_farmers(request_id, response, exception):
1154 \"\"\"Do something with the farmers list response.\"\"\"
1155 if exception is not None:
1156 # Do something with the exception.
1157 pass
1158 else:
1159 # Do something with the response.
1160 pass
1161
1162 service = build('farm', 'v2')
1163
1164 batch = BatchHttpRequest()
1165
1166 batch.add(service.animals().list(), list_animals)
1167 batch.add(service.farmers().list(), list_farmers)
1168 batch.execute(http=http)
1169 """
1170
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001171 @util.positional(1)
1172 def __init__(self, callback=None, batch_uri=None):
1173 """Constructor for a BatchHttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001174
1175 Args:
1176 callback: callable, A callback to be called for each response, of the
1177 form callback(id, response, exception). The first parameter is the
1178 request id, and the second is the deserialized response object. The
1179 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1180 occurred while processing the request, or None if no error occurred.
1181 batch_uri: string, URI to send batch requests to.
1182 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001183 if batch_uri is None:
1184 batch_uri = _LEGACY_BATCH_URI
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001185
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001186 if batch_uri == _LEGACY_BATCH_URI:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001187 LOGGER.warning(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001188 "You have constructed a BatchHttpRequest using the legacy batch "
Brad Vogel6ddadd72020-05-15 10:02:04 -07001189 "endpoint %s. This endpoint will be turned down on August 12, 2020. "
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001190 "Please provide the API-specific endpoint or use "
1191 "service.new_batch_http_request(). For more details see "
1192 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1193 "and https://developers.google.com/api-client-library/python/guide/batch.",
1194 _LEGACY_BATCH_URI,
1195 )
1196 self._batch_uri = batch_uri
John Asmuth864311d2014-04-24 15:46:08 -04001197
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001198 # Global callback to be called for each individual response in the batch.
1199 self._callback = callback
John Asmuth864311d2014-04-24 15:46:08 -04001200
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001201 # A map from id to request.
1202 self._requests = {}
John Asmuth864311d2014-04-24 15:46:08 -04001203
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001204 # A map from id to callback.
1205 self._callbacks = {}
John Asmuth864311d2014-04-24 15:46:08 -04001206
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001207 # List of request ids, in the order in which they were added.
1208 self._order = []
John Asmuth864311d2014-04-24 15:46:08 -04001209
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001210 # The last auto generated id.
1211 self._last_auto_id = 0
John Asmuth864311d2014-04-24 15:46:08 -04001212
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001213 # Unique ID on which to base the Content-ID headers.
1214 self._base_id = None
John Asmuth864311d2014-04-24 15:46:08 -04001215
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001216 # A map from request id to (httplib2.Response, content) response pairs
1217 self._responses = {}
John Asmuth864311d2014-04-24 15:46:08 -04001218
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001219 # A map of id(Credentials) that have been refreshed.
1220 self._refreshed_credentials = {}
John Asmuth864311d2014-04-24 15:46:08 -04001221
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001222 def _refresh_and_apply_credentials(self, request, http):
1223 """Refresh the credentials and apply to the request.
John Asmuth864311d2014-04-24 15:46:08 -04001224
1225 Args:
1226 request: HttpRequest, the request.
1227 http: httplib2.Http, the global http object for the batch.
1228 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001229 # For the credentials to refresh, but only once per refresh_token
1230 # If there is no http per the request then refresh the http passed in
1231 # via execute()
1232 creds = None
1233 request_credentials = False
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001235 if request.http is not None:
1236 creds = _auth.get_credentials_from_http(request.http)
1237 request_credentials = True
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001238
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001239 if creds is None and http is not None:
1240 creds = _auth.get_credentials_from_http(http)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001241
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001242 if creds is not None:
1243 if id(creds) not in self._refreshed_credentials:
1244 _auth.refresh_credentials(creds)
1245 self._refreshed_credentials[id(creds)] = 1
John Asmuth864311d2014-04-24 15:46:08 -04001246
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001247 # Only apply the credentials if we are using the http object passed in,
1248 # otherwise apply() will get called during _serialize_request().
1249 if request.http is None or not request_credentials:
1250 _auth.apply_credentials(creds, request.headers)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001251
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001252 def _id_to_header(self, id_):
1253 """Convert an id to a Content-ID header value.
John Asmuth864311d2014-04-24 15:46:08 -04001254
1255 Args:
1256 id_: string, identifier of individual request.
1257
1258 Returns:
1259 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1260 the value because Content-ID headers are supposed to be universally
1261 unique.
1262 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001263 if self._base_id is None:
1264 self._base_id = uuid.uuid4()
John Asmuth864311d2014-04-24 15:46:08 -04001265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001266 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1267 # line folding works properly on Python 3; see
Marie J.I48f503f2020-05-15 13:32:11 -04001268 # https://github.com/googleapis/google-api-python-client/issues/164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001269 return "<%s + %s>" % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001270
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001271 def _header_to_id(self, header):
1272 """Convert a Content-ID header value to an id.
John Asmuth864311d2014-04-24 15:46:08 -04001273
1274 Presumes the Content-ID header conforms to the format that _id_to_header()
1275 returns.
1276
1277 Args:
1278 header: string, Content-ID header value.
1279
1280 Returns:
1281 The extracted id value.
1282
1283 Raises:
1284 BatchError if the header is not in the expected format.
1285 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001286 if header[0] != "<" or header[-1] != ">":
1287 raise BatchError("Invalid value for Content-ID: %s" % header)
1288 if "+" not in header:
1289 raise BatchError("Invalid value for Content-ID: %s" % header)
1290 base, id_ = header[1:-1].split(" + ", 1)
John Asmuth864311d2014-04-24 15:46:08 -04001291
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001292 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001293
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001294 def _serialize_request(self, request):
1295 """Convert an HttpRequest object into a string.
John Asmuth864311d2014-04-24 15:46:08 -04001296
1297 Args:
1298 request: HttpRequest, the request to serialize.
1299
1300 Returns:
1301 The request as a string in application/http format.
1302 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001303 # Construct status line
1304 parsed = urlparse(request.uri)
1305 request_line = urlunparse(
1306 ("", "", parsed.path, parsed.params, parsed.query, "")
John Asmuth864311d2014-04-24 15:46:08 -04001307 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001308 status_line = request.method + " " + request_line + " HTTP/1.1\n"
1309 major, minor = request.headers.get("content-type", "application/json").split(
1310 "/"
1311 )
1312 msg = MIMENonMultipart(major, minor)
1313 headers = request.headers.copy()
John Asmuth864311d2014-04-24 15:46:08 -04001314
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001315 if request.http is not None:
1316 credentials = _auth.get_credentials_from_http(request.http)
1317 if credentials is not None:
1318 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001320 # MIMENonMultipart adds its own Content-Type header.
1321 if "content-type" in headers:
1322 del headers["content-type"]
John Asmuth864311d2014-04-24 15:46:08 -04001323
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001324 for key, value in six.iteritems(headers):
1325 msg[key] = value
1326 msg["Host"] = parsed.netloc
1327 msg.set_unixfrom(None)
John Asmuth864311d2014-04-24 15:46:08 -04001328
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001329 if request.body is not None:
1330 msg.set_payload(request.body)
1331 msg["content-length"] = str(len(request.body))
John Asmuth864311d2014-04-24 15:46:08 -04001332
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001333 # Serialize the mime message.
1334 fp = StringIO()
1335 # maxheaderlen=0 means don't line wrap headers.
1336 g = Generator(fp, maxheaderlen=0)
1337 g.flatten(msg, unixfrom=False)
1338 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001339
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001340 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001341
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001342 def _deserialize_response(self, payload):
1343 """Convert string into httplib2 response and content.
John Asmuth864311d2014-04-24 15:46:08 -04001344
1345 Args:
1346 payload: string, headers and body as a string.
1347
1348 Returns:
1349 A pair (resp, content), such as would be returned from httplib2.request.
1350 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001351 # Strip off the status line
1352 status_line, payload = payload.split("\n", 1)
1353 protocol, status, reason = status_line.split(" ", 2)
John Asmuth864311d2014-04-24 15:46:08 -04001354
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001355 # Parse the rest of the response
1356 parser = FeedParser()
1357 parser.feed(payload)
1358 msg = parser.close()
1359 msg["status"] = status
John Asmuth864311d2014-04-24 15:46:08 -04001360
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001361 # Create httplib2.Response from the parsed headers.
1362 resp = httplib2.Response(msg)
1363 resp.reason = reason
1364 resp.version = int(protocol.split("/", 1)[1].replace(".", ""))
John Asmuth864311d2014-04-24 15:46:08 -04001365
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001366 content = payload.split("\r\n\r\n", 1)[1]
John Asmuth864311d2014-04-24 15:46:08 -04001367
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001368 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001369
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001370 def _new_id(self):
1371 """Create a new id.
John Asmuth864311d2014-04-24 15:46:08 -04001372
1373 Auto incrementing number that avoids conflicts with ids already used.
1374
1375 Returns:
1376 string, a new unique id.
1377 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001378 self._last_auto_id += 1
1379 while str(self._last_auto_id) in self._requests:
1380 self._last_auto_id += 1
1381 return str(self._last_auto_id)
John Asmuth864311d2014-04-24 15:46:08 -04001382
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001383 @util.positional(2)
1384 def add(self, request, callback=None, request_id=None):
1385 """Add a new request.
John Asmuth864311d2014-04-24 15:46:08 -04001386
1387 Every callback added will be paired with a unique id, the request_id. That
1388 unique id will be passed back to the callback when the response comes back
1389 from the server. The default behavior is to have the library generate it's
1390 own unique id. If the caller passes in a request_id then they must ensure
1391 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001392 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001393 request id, to avoid such an error.
1394
1395 Args:
1396 request: HttpRequest, Request to add to the batch.
1397 callback: callable, A callback to be called for this response, of the
1398 form callback(id, response, exception). The first parameter is the
1399 request id, and the second is the deserialized response object. The
1400 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1401 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001402 request_id: string, A unique id for the request. The id will be passed
1403 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001404
1405 Returns:
1406 None
1407
1408 Raises:
1409 BatchError if a media request is added to a batch.
1410 KeyError is the request_id is not unique.
1411 """
Xinan Line2dccec2018-12-07 05:28:33 +09001412
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001413 if len(self._order) >= MAX_BATCH_LIMIT:
1414 raise BatchError(
1415 "Exceeded the maximum calls(%d) in a single batch request."
1416 % MAX_BATCH_LIMIT
1417 )
1418 if request_id is None:
1419 request_id = self._new_id()
1420 if request.resumable is not None:
1421 raise BatchError("Media requests cannot be used in a batch request.")
1422 if request_id in self._requests:
1423 raise KeyError("A request with this ID already exists: %s" % request_id)
1424 self._requests[request_id] = request
1425 self._callbacks[request_id] = callback
1426 self._order.append(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001427
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001428 def _execute(self, http, order, requests):
1429 """Serialize batch request, send to server, process response.
John Asmuth864311d2014-04-24 15:46:08 -04001430
1431 Args:
1432 http: httplib2.Http, an http object to be used to make the request with.
1433 order: list, list of request ids in the order they were added to the
1434 batch.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001435 requests: list, list of request objects to send.
John Asmuth864311d2014-04-24 15:46:08 -04001436
1437 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001438 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001439 googleapiclient.errors.BatchError if the response is the wrong format.
1440 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001441 message = MIMEMultipart("mixed")
1442 # Message should not write out it's own headers.
1443 setattr(message, "_write_headers", lambda self: None)
John Asmuth864311d2014-04-24 15:46:08 -04001444
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001445 # Add all the individual requests.
1446 for request_id in order:
1447 request = requests[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001448
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001449 msg = MIMENonMultipart("application", "http")
1450 msg["Content-Transfer-Encoding"] = "binary"
1451 msg["Content-ID"] = self._id_to_header(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001452
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001453 body = self._serialize_request(request)
1454 msg.set_payload(body)
1455 message.attach(msg)
John Asmuth864311d2014-04-24 15:46:08 -04001456
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001457 # encode the body: note that we can't use `as_string`, because
1458 # it plays games with `From ` lines.
1459 fp = StringIO()
1460 g = Generator(fp, mangle_from_=False)
1461 g.flatten(message, unixfrom=False)
1462 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001463
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001464 headers = {}
1465 headers["content-type"] = (
1466 "multipart/mixed; " 'boundary="%s"'
1467 ) % message.get_boundary()
John Asmuth864311d2014-04-24 15:46:08 -04001468
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001469 resp, content = http.request(
1470 self._batch_uri, method="POST", body=body, headers=headers
1471 )
John Asmuth864311d2014-04-24 15:46:08 -04001472
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001473 if resp.status >= 300:
1474 raise HttpError(resp, content, uri=self._batch_uri)
John Asmuth864311d2014-04-24 15:46:08 -04001475
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001476 # Prepend with a content-type header so FeedParser can handle it.
1477 header = "content-type: %s\r\n\r\n" % resp["content-type"]
1478 # PY3's FeedParser only accepts unicode. So we should decode content
1479 # here, and encode each payload again.
1480 if six.PY3:
1481 content = content.decode("utf-8")
1482 for_parser = header + content
John Asmuth864311d2014-04-24 15:46:08 -04001483
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001484 parser = FeedParser()
1485 parser.feed(for_parser)
1486 mime_response = parser.close()
John Asmuth864311d2014-04-24 15:46:08 -04001487
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001488 if not mime_response.is_multipart():
1489 raise BatchError(
1490 "Response not in multipart/mixed format.", resp=resp, content=content
1491 )
John Asmuth864311d2014-04-24 15:46:08 -04001492
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001493 for part in mime_response.get_payload():
1494 request_id = self._header_to_id(part["Content-ID"])
1495 response, content = self._deserialize_response(part.get_payload())
1496 # We encode content here to emulate normal http response.
1497 if isinstance(content, six.text_type):
1498 content = content.encode("utf-8")
1499 self._responses[request_id] = (response, content)
John Asmuth864311d2014-04-24 15:46:08 -04001500
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001501 @util.positional(1)
1502 def execute(self, http=None):
1503 """Execute all the requests as a single batched HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001504
1505 Args:
1506 http: httplib2.Http, an http object to be used in place of the one the
1507 HttpRequest request object was constructed with. If one isn't supplied
1508 then use a http object from the requests in this batch.
1509
1510 Returns:
1511 None
1512
1513 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001514 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001515 googleapiclient.errors.BatchError if the response is the wrong format.
1516 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001517 # If we have no requests return
1518 if len(self._order) == 0:
1519 return None
John Asmuth864311d2014-04-24 15:46:08 -04001520
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001521 # If http is not supplied use the first valid one given in the requests.
1522 if http is None:
1523 for request_id in self._order:
1524 request = self._requests[request_id]
1525 if request is not None:
1526 http = request.http
1527 break
John Asmuth864311d2014-04-24 15:46:08 -04001528
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001529 if http is None:
1530 raise ValueError("Missing a valid http object.")
John Asmuth864311d2014-04-24 15:46:08 -04001531
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001532 # Special case for OAuth2Credentials-style objects which have not yet been
1533 # refreshed with an initial access_token.
1534 creds = _auth.get_credentials_from_http(http)
1535 if creds is not None:
1536 if not _auth.is_valid(creds):
1537 LOGGER.info("Attempting refresh to obtain initial access_token")
1538 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001539
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001540 self._execute(http, self._order, self._requests)
John Asmuth864311d2014-04-24 15:46:08 -04001541
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001542 # Loop over all the requests and check for 401s. For each 401 request the
1543 # credentials should be refreshed and then sent again in a separate batch.
1544 redo_requests = {}
1545 redo_order = []
John Asmuth864311d2014-04-24 15:46:08 -04001546
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001547 for request_id in self._order:
1548 resp, content = self._responses[request_id]
1549 if resp["status"] == "401":
1550 redo_order.append(request_id)
1551 request = self._requests[request_id]
1552 self._refresh_and_apply_credentials(request, http)
1553 redo_requests[request_id] = request
John Asmuth864311d2014-04-24 15:46:08 -04001554
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001555 if redo_requests:
1556 self._execute(http, redo_order, redo_requests)
John Asmuth864311d2014-04-24 15:46:08 -04001557
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001558 # Now process all callbacks that are erroring, and raise an exception for
1559 # ones that return a non-2xx response? Or add extra parameter to callback
1560 # that contains an HttpError?
John Asmuth864311d2014-04-24 15:46:08 -04001561
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001562 for request_id in self._order:
1563 resp, content = self._responses[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001564
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001565 request = self._requests[request_id]
1566 callback = self._callbacks[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001567
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001568 response = None
1569 exception = None
1570 try:
1571 if resp.status >= 300:
1572 raise HttpError(resp, content, uri=request.uri)
1573 response = request.postproc(resp, content)
1574 except HttpError as e:
1575 exception = e
John Asmuth864311d2014-04-24 15:46:08 -04001576
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001577 if callback is not None:
1578 callback(request_id, response, exception)
1579 if self._callback is not None:
1580 self._callback(request_id, response, exception)
John Asmuth864311d2014-04-24 15:46:08 -04001581
1582
1583class HttpRequestMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001584 """Mock of HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001585
1586 Do not construct directly, instead use RequestMockBuilder.
1587 """
1588
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001589 def __init__(self, resp, content, postproc):
1590 """Constructor for HttpRequestMock
John Asmuth864311d2014-04-24 15:46:08 -04001591
1592 Args:
1593 resp: httplib2.Response, the response to emulate coming from the request
1594 content: string, the response body
1595 postproc: callable, the post processing function usually supplied by
1596 the model class. See model.JsonModel.response() as an example.
1597 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001598 self.resp = resp
1599 self.content = content
1600 self.postproc = postproc
1601 if resp is None:
1602 self.resp = httplib2.Response({"status": 200, "reason": "OK"})
1603 if "reason" in self.resp:
1604 self.resp.reason = self.resp["reason"]
John Asmuth864311d2014-04-24 15:46:08 -04001605
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001606 def execute(self, http=None):
1607 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -04001608
1609 Same behavior as HttpRequest.execute(), but the response is
1610 mocked and not really from an HTTP request/response.
1611 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001612 return self.postproc(self.resp, self.content)
John Asmuth864311d2014-04-24 15:46:08 -04001613
1614
1615class RequestMockBuilder(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001616 """A simple mock of HttpRequest
John Asmuth864311d2014-04-24 15:46:08 -04001617
1618 Pass in a dictionary to the constructor that maps request methodIds to
1619 tuples of (httplib2.Response, content, opt_expected_body) that should be
1620 returned when that method is called. None may also be passed in for the
1621 httplib2.Response, in which case a 200 OK response will be generated.
1622 If an opt_expected_body (str or dict) is provided, it will be compared to
1623 the body and UnexpectedBodyError will be raised on inequality.
1624
1625 Example:
1626 response = '{"data": {"id": "tag:google.c...'
1627 requestBuilder = RequestMockBuilder(
1628 {
1629 'plus.activities.get': (None, response),
1630 }
1631 )
1632 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1633
1634 Methods that you do not supply a response for will return a
1635 200 OK with an empty string as the response content or raise an excpetion
1636 if check_unexpected is set to True. The methodId is taken from the rpcName
1637 in the discovery document.
1638
1639 For more details see the project wiki.
1640 """
1641
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001642 def __init__(self, responses, check_unexpected=False):
1643 """Constructor for RequestMockBuilder
John Asmuth864311d2014-04-24 15:46:08 -04001644
1645 The constructed object should be a callable object
1646 that can replace the class HttpResponse.
1647
1648 responses - A dictionary that maps methodIds into tuples
1649 of (httplib2.Response, content). The methodId
1650 comes from the 'rpcName' field in the discovery
1651 document.
1652 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1653 should be raised on unsupplied method.
1654 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001655 self.responses = responses
1656 self.check_unexpected = check_unexpected
John Asmuth864311d2014-04-24 15:46:08 -04001657
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001658 def __call__(
1659 self,
1660 http,
1661 postproc,
1662 uri,
1663 method="GET",
1664 body=None,
1665 headers=None,
1666 methodId=None,
1667 resumable=None,
1668 ):
1669 """Implements the callable interface that discovery.build() expects
John Asmuth864311d2014-04-24 15:46:08 -04001670 of requestBuilder, which is to build an object compatible with
1671 HttpRequest.execute(). See that method for the description of the
1672 parameters and the expected response.
1673 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001674 if methodId in self.responses:
1675 response = self.responses[methodId]
1676 resp, content = response[:2]
1677 if len(response) > 2:
1678 # Test the body against the supplied expected_body.
1679 expected_body = response[2]
1680 if bool(expected_body) != bool(body):
1681 # Not expecting a body and provided one
1682 # or expecting a body and not provided one.
1683 raise UnexpectedBodyError(expected_body, body)
1684 if isinstance(expected_body, str):
1685 expected_body = json.loads(expected_body)
1686 body = json.loads(body)
1687 if body != expected_body:
1688 raise UnexpectedBodyError(expected_body, body)
1689 return HttpRequestMock(resp, content, postproc)
1690 elif self.check_unexpected:
1691 raise UnexpectedMethodError(methodId=methodId)
1692 else:
1693 model = JsonModel(False)
1694 return HttpRequestMock(None, "{}", model.response)
John Asmuth864311d2014-04-24 15:46:08 -04001695
1696
1697class HttpMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001698 """Mock of httplib2.Http"""
John Asmuth864311d2014-04-24 15:46:08 -04001699
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001700 def __init__(self, filename=None, headers=None):
1701 """
John Asmuth864311d2014-04-24 15:46:08 -04001702 Args:
1703 filename: string, absolute filename to read response from
1704 headers: dict, header to return with response
1705 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001706 if headers is None:
1707 headers = {"status": "200"}
1708 if filename:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001709 with open(filename, "rb") as f:
1710 self.data = f.read()
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001711 else:
1712 self.data = None
1713 self.response_headers = headers
1714 self.headers = None
1715 self.uri = None
1716 self.method = None
1717 self.body = None
1718 self.headers = None
John Asmuth864311d2014-04-24 15:46:08 -04001719
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001720 def request(
1721 self,
1722 uri,
1723 method="GET",
1724 body=None,
1725 headers=None,
1726 redirections=1,
1727 connection_type=None,
1728 ):
1729 self.uri = uri
1730 self.method = method
1731 self.body = body
1732 self.headers = headers
1733 return httplib2.Response(self.response_headers), self.data
John Asmuth864311d2014-04-24 15:46:08 -04001734
Bu Sun Kim98888da2020-09-23 11:10:39 -06001735 def close(self):
1736 return None
John Asmuth864311d2014-04-24 15:46:08 -04001737
1738class HttpMockSequence(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001739 """Mock of httplib2.Http
John Asmuth864311d2014-04-24 15:46:08 -04001740
1741 Mocks a sequence of calls to request returning different responses for each
1742 call. Create an instance initialized with the desired response headers
1743 and content and then use as if an httplib2.Http instance.
1744
1745 http = HttpMockSequence([
1746 ({'status': '401'}, ''),
1747 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1748 ({'status': '200'}, 'echo_request_headers'),
1749 ])
1750 resp, content = http.request("http://examples.com")
1751
1752 There are special values you can pass in for content to trigger
1753 behavours that are helpful in testing.
1754
1755 'echo_request_headers' means return the request headers in the response body
1756 'echo_request_headers_as_json' means return the request headers in
1757 the response body
1758 'echo_request_body' means return the request body in the response body
1759 'echo_request_uri' means return the request uri in the response body
1760 """
1761
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001762 def __init__(self, iterable):
1763 """
John Asmuth864311d2014-04-24 15:46:08 -04001764 Args:
1765 iterable: iterable, a sequence of pairs of (headers, body)
1766 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001767 self._iterable = iterable
1768 self.follow_redirects = True
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001769 self.request_sequence = list()
John Asmuth864311d2014-04-24 15:46:08 -04001770
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001771 def request(
1772 self,
1773 uri,
1774 method="GET",
1775 body=None,
1776 headers=None,
1777 redirections=1,
1778 connection_type=None,
1779 ):
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001780 # Remember the request so after the fact this mock can be examined
1781 self.request_sequence.append((uri, method, body, headers))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001782 resp, content = self._iterable.pop(0)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001783 content = six.ensure_binary(content)
1784
1785 if content == b"echo_request_headers":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001786 content = headers
Matt McDonaldef6420a2020-04-14 16:28:13 -04001787 elif content == b"echo_request_headers_as_json":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001788 content = json.dumps(headers)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001789 elif content == b"echo_request_body":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001790 if hasattr(body, "read"):
1791 content = body.read()
1792 else:
1793 content = body
Matt McDonaldef6420a2020-04-14 16:28:13 -04001794 elif content == b"echo_request_uri":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001795 content = uri
1796 if isinstance(content, six.text_type):
1797 content = content.encode("utf-8")
1798 return httplib2.Response(resp), content
John Asmuth864311d2014-04-24 15:46:08 -04001799
1800
1801def set_user_agent(http, user_agent):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001802 """Set the user-agent on every request.
John Asmuth864311d2014-04-24 15:46:08 -04001803
1804 Args:
1805 http - An instance of httplib2.Http
1806 or something that acts like it.
1807 user_agent: string, the value for the user-agent header.
1808
1809 Returns:
1810 A modified instance of http that was passed in.
1811
1812 Example:
1813
1814 h = httplib2.Http()
1815 h = set_user_agent(h, "my-app-name/6.0")
1816
1817 Most of the time the user-agent will be set doing auth, this is for the rare
1818 cases where you are accessing an unauthenticated endpoint.
1819 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001820 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001821
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001822 # The closure that will replace 'httplib2.Http.request'.
1823 def new_request(
1824 uri,
1825 method="GET",
1826 body=None,
1827 headers=None,
1828 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1829 connection_type=None,
1830 ):
1831 """Modify the request headers to add the user-agent."""
1832 if headers is None:
1833 headers = {}
1834 if "user-agent" in headers:
1835 headers["user-agent"] = user_agent + " " + headers["user-agent"]
1836 else:
1837 headers["user-agent"] = user_agent
1838 resp, content = request_orig(
1839 uri,
1840 method=method,
1841 body=body,
1842 headers=headers,
1843 redirections=redirections,
1844 connection_type=connection_type,
1845 )
1846 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001847
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001848 http.request = new_request
1849 return http
John Asmuth864311d2014-04-24 15:46:08 -04001850
1851
1852def tunnel_patch(http):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001853 """Tunnel PATCH requests over POST.
John Asmuth864311d2014-04-24 15:46:08 -04001854 Args:
1855 http - An instance of httplib2.Http
1856 or something that acts like it.
1857
1858 Returns:
1859 A modified instance of http that was passed in.
1860
1861 Example:
1862
1863 h = httplib2.Http()
1864 h = tunnel_patch(h, "my-app-name/6.0")
1865
1866 Useful if you are running on a platform that doesn't support PATCH.
1867 Apply this last if you are using OAuth 1.0, as changing the method
1868 will result in a different signature.
1869 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001870 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001871
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001872 # The closure that will replace 'httplib2.Http.request'.
1873 def new_request(
1874 uri,
1875 method="GET",
1876 body=None,
1877 headers=None,
1878 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1879 connection_type=None,
1880 ):
1881 """Modify the request headers to add the user-agent."""
1882 if headers is None:
1883 headers = {}
1884 if method == "PATCH":
1885 if "oauth_token" in headers.get("authorization", ""):
1886 LOGGER.warning(
1887 "OAuth 1.0 request made with Credentials after tunnel_patch."
1888 )
1889 headers["x-http-method-override"] = "PATCH"
1890 method = "POST"
1891 resp, content = request_orig(
1892 uri,
1893 method=method,
1894 body=body,
1895 headers=headers,
1896 redirections=redirections,
1897 connection_type=connection_type,
1898 )
1899 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001900
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001901 http.request = new_request
1902 return http
Igor Maravić22435292017-01-19 22:28:22 +01001903
1904
1905def build_http():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001906 """Builds httplib2.Http object
Igor Maravić22435292017-01-19 22:28:22 +01001907
1908 Returns:
1909 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1910 To override default timeout call
1911
1912 socket.setdefaulttimeout(timeout_in_sec)
1913
1914 before interacting with this method.
1915 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001916 if socket.getdefaulttimeout() is not None:
1917 http_timeout = socket.getdefaulttimeout()
1918 else:
1919 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001920 http = httplib2.Http(timeout=http_timeout)
1921 # 308's are used by several Google APIs (Drive, YouTube)
1922 # for Resumable Uploads rather than Permanent Redirects.
1923 # This asks httplib2 to exclude 308s from the status codes
1924 # it treats as redirects
Bu Sun Kima480d532020-03-13 12:52:22 -07001925 try:
1926 http.redirect_codes = http.redirect_codes - {308}
1927 except AttributeError:
1928 # Apache Beam tests depend on this library and cannot
1929 # currently upgrade their httplib2 version
1930 # http.redirect_codes does not exist in previous versions
1931 # of httplib2, so pass
1932 pass
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001933
1934 return http