blob: 5d4227fb888736d6eb815cda60d6cdba93909e86 [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 copy
John Asmuth864311d2014-04-24 15:46:08 -040032import httplib2
Craig Citro6ae34d72014-08-18 23:10:09 -070033import json
John Asmuth864311d2014-04-24 15:46:08 -040034import logging
John Asmuth864311d2014-04-24 15:46:08 -040035import mimetypes
36import os
37import random
eesheeshc6425a02016-02-12 15:07:06 +000038import socket
John Asmuth864311d2014-04-24 15:46:08 -040039import time
John Asmuth864311d2014-04-24 15:46:08 -040040import uuid
41
Tay Ray Chuan3146c922016-04-20 16:38:19 +000042# TODO(issue 221): Remove this conditional import jibbajabba.
43try:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070044 import ssl
Tay Ray Chuan3146c922016-04-20 16:38:19 +000045except ImportError:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070046 _ssl_SSLError = object()
Tay Ray Chuan3146c922016-04-20 16:38:19 +000047else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070048 _ssl_SSLError = ssl.SSLError
Tay Ray Chuan3146c922016-04-20 16:38:19 +000049
John Asmuth864311d2014-04-24 15:46:08 -040050from email.generator import Generator
51from email.mime.multipart import MIMEMultipart
52from email.mime.nonmultipart import MIMENonMultipart
53from email.parser import FeedParser
Pat Ferateb240c172015-03-03 16:23:51 -080054
Helen Koikede13e3b2018-04-26 16:05:16 -030055from googleapiclient import _helpers as util
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070056
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070057from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080058from googleapiclient.errors import BatchError
59from googleapiclient.errors import HttpError
60from googleapiclient.errors import InvalidChunkSizeError
61from googleapiclient.errors import ResumableUploadError
62from googleapiclient.errors import UnexpectedBodyError
63from googleapiclient.errors import UnexpectedMethodError
64from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040065
66
Emmett Butler09699152016-02-08 14:26:00 -080067LOGGER = logging.getLogger(__name__)
68
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070069DEFAULT_CHUNK_SIZE = 100 * 1024 * 1024
John Asmuth864311d2014-04-24 15:46:08 -040070
71MAX_URI_LENGTH = 2048
72
Xinan Line2dccec2018-12-07 05:28:33 +090073MAX_BATCH_LIMIT = 1000
74
eesheeshc6425a02016-02-12 15:07:06 +000075_TOO_MANY_REQUESTS = 429
76
Igor Maravić22435292017-01-19 22:28:22 +010077DEFAULT_HTTP_TIMEOUT_SEC = 60
78
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070079_LEGACY_BATCH_URI = "https://www.googleapis.com/batch"
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070080
Damian Gadomskic7516a22020-03-23 20:39:21 +010081if six.PY2:
82 # That's a builtin python3 exception, nonexistent in python2.
83 # Defined to None to avoid NameError while trying to catch it
84 ConnectionError = None
85
eesheeshc6425a02016-02-12 15:07:06 +000086
87def _should_retry_response(resp_status, content):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070088 """Determines whether a response should be retried.
eesheeshc6425a02016-02-12 15:07:06 +000089
90 Args:
91 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080092 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000093
94 Returns:
95 True if the response should be retried, otherwise False.
96 """
Anthonios Partheniouc5184722021-03-15 17:04:03 -040097 reason = None
98
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070099 # Retry on 5xx errors.
100 if resp_status >= 500:
101 return True
eesheeshc6425a02016-02-12 15:07:06 +0000102
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700103 # Retry on 429 errors.
104 if resp_status == _TOO_MANY_REQUESTS:
105 return True
eesheeshc6425a02016-02-12 15:07:06 +0000106
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700107 # For 403 errors, we have to check for the `reason` in the response to
108 # determine if we should retry.
109 if resp_status == six.moves.http_client.FORBIDDEN:
110 # If there's no details about the 403 type, don't retry.
111 if not content:
112 return False
eesheeshc6425a02016-02-12 15:07:06 +0000113
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700114 # Content is in JSON format.
115 try:
116 data = json.loads(content.decode("utf-8"))
117 if isinstance(data, dict):
Anthonios Partheniouc5184722021-03-15 17:04:03 -0400118 # There are many variations of the error json so we need
119 # to determine the keyword which has the error detail. Make sure
120 # that the order of the keywords below isn't changed as it can
121 # break user code. If the "errors" key exists, we must use that
122 # first.
123 # See Issue #1243
124 # https://github.com/googleapis/google-api-python-client/issues/1243
125 error_detail_keyword = next((kw for kw in ["errors", "status", "message"] if kw in data["error"]), "")
126
127 if error_detail_keyword:
128 reason = data["error"][error_detail_keyword]
129
130 if isinstance(reason, list) and len(reason) > 0:
131 reason = reason[0]
132 if "reason" in reason:
133 reason = reason["reason"]
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700134 else:
135 reason = data[0]["error"]["errors"]["reason"]
136 except (UnicodeDecodeError, ValueError, KeyError):
137 LOGGER.warning("Invalid JSON content from response: %s", content)
138 return False
eesheeshc6425a02016-02-12 15:07:06 +0000139
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700140 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
eesheeshc6425a02016-02-12 15:07:06 +0000141
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700142 # Only retry on rate limit related failures.
143 if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
144 return True
eesheeshc6425a02016-02-12 15:07:06 +0000145
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700146 # Everything else is a success or non-retriable so break.
147 return False
eesheeshc6425a02016-02-12 15:07:06 +0000148
John Asmuth864311d2014-04-24 15:46:08 -0400149
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700150def _retry_request(
151 http, num_retries, req_type, sleep, rand, uri, method, *args, **kwargs
152):
153 """Retries an HTTP request multiple times while handling errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100154
155 If after all retries the request still fails, last error is either returned as
156 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
157
158 Args:
159 http: Http object to be used to execute request.
160 num_retries: Maximum number of retries.
161 req_type: Type of the request (used for logging retries).
162 sleep, rand: Functions to sleep for random time between retries.
163 uri: URI to be requested.
164 method: HTTP method to be used.
165 args, kwargs: Additional arguments passed to http.request.
166
167 Returns:
168 resp, content - Response from the http request (may be HTTP 5xx).
169 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700170 resp = None
171 content = None
172 exception = None
173 for retry_num in range(num_retries + 1):
174 if retry_num > 0:
175 # Sleep before retrying.
176 sleep_time = rand() * 2 ** retry_num
177 LOGGER.warning(
178 "Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s",
179 sleep_time,
180 retry_num,
181 num_retries,
182 req_type,
183 method,
184 uri,
185 resp.status if resp else exception,
186 )
187 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100188
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700189 try:
190 exception = None
191 resp, content = http.request(uri, method, *args, **kwargs)
192 # Retry on SSL errors and socket timeout errors.
193 except _ssl_SSLError as ssl_error:
194 exception = ssl_error
195 except socket.timeout as socket_timeout:
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600196 # Needs to be before socket.error as it's a subclass of OSError
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700197 # socket.timeout has no errorcode
198 exception = socket_timeout
Damian Gadomskic7516a22020-03-23 20:39:21 +0100199 except ConnectionError as connection_error:
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600200 # Needs to be before socket.error as it's a subclass of OSError
Damian Gadomskic7516a22020-03-23 20:39:21 +0100201 exception = connection_error
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600202 except OSError as socket_error:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700203 # errno's contents differ by platform, so we have to match by name.
Aaron Niskode-Dossettae9cd992021-01-13 04:58:15 -0600204 # Some of these same errors may have been caught above, e.g. ECONNRESET *should* be
205 # raised as a ConnectionError, but some libraries will raise it as a socket.error
206 # with an errno corresponding to ECONNRESET
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700207 if socket.errno.errorcode.get(socket_error.errno) not in {
208 "WSAETIMEDOUT",
209 "ETIMEDOUT",
210 "EPIPE",
211 "ECONNABORTED",
Aaron Niskode-Dossettae9cd992021-01-13 04:58:15 -0600212 "ECONNREFUSED",
213 "ECONNRESET",
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700214 }:
215 raise
216 exception = socket_error
217 except httplib2.ServerNotFoundError as server_not_found_error:
218 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000219
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700220 if exception:
221 if retry_num == num_retries:
222 raise exception
223 else:
224 continue
eesheeshc6425a02016-02-12 15:07:06 +0000225
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700226 if not _should_retry_response(resp.status, content):
227 break
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100228
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700229 return resp, content
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100230
231
John Asmuth864311d2014-04-24 15:46:08 -0400232class MediaUploadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700233 """Status of a resumable upload."""
John Asmuth864311d2014-04-24 15:46:08 -0400234
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700235 def __init__(self, resumable_progress, total_size):
236 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400237
238 Args:
239 resumable_progress: int, bytes sent so far.
240 total_size: int, total bytes in complete upload, or None if the total
241 upload size isn't known ahead of time.
242 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700243 self.resumable_progress = resumable_progress
244 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400245
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700246 def progress(self):
247 """Percent of upload completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400248
249 Returns:
250 the percentage complete as a float, returning 0.0 if the total size of
251 the upload is unknown.
252 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700253 if self.total_size is not None and self.total_size != 0:
254 return float(self.resumable_progress) / float(self.total_size)
255 else:
256 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400257
258
259class MediaDownloadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700260 """Status of a resumable download."""
John Asmuth864311d2014-04-24 15:46:08 -0400261
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700262 def __init__(self, resumable_progress, total_size):
263 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400264
265 Args:
266 resumable_progress: int, bytes received so far.
267 total_size: int, total bytes in complete download.
268 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700269 self.resumable_progress = resumable_progress
270 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400271
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700272 def progress(self):
273 """Percent of download completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400274
275 Returns:
276 the percentage complete as a float, returning 0.0 if the total size of
277 the download is unknown.
278 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700279 if self.total_size is not None and self.total_size != 0:
280 return float(self.resumable_progress) / float(self.total_size)
281 else:
282 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400283
284
285class MediaUpload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700286 """Describes a media object to upload.
John Asmuth864311d2014-04-24 15:46:08 -0400287
288 Base class that defines the interface of MediaUpload subclasses.
289
290 Note that subclasses of MediaUpload may allow you to control the chunksize
291 when uploading a media object. It is important to keep the size of the chunk
292 as large as possible to keep the upload efficient. Other factors may influence
293 the size of the chunk you use, particularly if you are working in an
294 environment where individual HTTP requests may have a hardcoded time limit,
295 such as under certain classes of requests under Google App Engine.
296
297 Streams are io.Base compatible objects that support seek(). Some MediaUpload
298 subclasses support using streams directly to upload data. Support for
299 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
300 platform that stream will be used for uploading the media object. The support
301 for streaming is indicated by has_stream() returning True. The stream() method
302 should return an io.Base object that supports seek(). On platforms where the
303 underlying httplib module supports streaming, for example Python 2.6 and
304 later, the stream will be passed into the http library which will result in
305 less memory being used and possibly faster uploads.
306
307 If you need to upload media that can't be uploaded using any of the existing
308 MediaUpload sub-class then you can sub-class MediaUpload for your particular
309 needs.
310 """
311
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700312 def chunksize(self):
313 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400314
315 Returns:
316 Chunk size in bytes.
317 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700318 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700320 def mimetype(self):
321 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400322
323 Returns:
324 Mime type.
325 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700326 return "application/octet-stream"
John Asmuth864311d2014-04-24 15:46:08 -0400327
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700328 def size(self):
329 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400330
331 Returns:
332 Size of the body, or None of the size is unknown.
333 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700334 return None
John Asmuth864311d2014-04-24 15:46:08 -0400335
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700336 def resumable(self):
337 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400338
339 Returns:
340 True if resumable upload or False.
341 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700342 return False
John Asmuth864311d2014-04-24 15:46:08 -0400343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700344 def getbytes(self, begin, end):
345 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400346
347 Args:
348 begin: int, offset from beginning of file.
349 length: int, number of bytes to read, starting at begin.
350
351 Returns:
352 A string of bytes read. May be shorter than length if EOF was reached
353 first.
354 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700355 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400356
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700357 def has_stream(self):
358 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400359
360 Streaming means it is an io.IOBase subclass that supports seek, i.e.
361 seekable() returns True.
362
363 Returns:
364 True if the call to stream() will return an instance of a seekable io.Base
365 subclass.
366 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700367 return False
John Asmuth864311d2014-04-24 15:46:08 -0400368
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700369 def stream(self):
370 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400371
372 Returns:
373 The returned value is an io.IOBase subclass that supports seek, i.e.
374 seekable() returns True.
375 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700376 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400377
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700378 @util.positional(1)
379 def _to_json(self, strip=None):
380 """Utility function for creating a JSON representation of a MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400381
382 Args:
383 strip: array, An array of names of members to not include in the JSON.
384
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 t = type(self)
390 d = copy.copy(self.__dict__)
391 if strip is not None:
392 for member in strip:
393 del d[member]
394 d["_class"] = t.__name__
395 d["_module"] = t.__module__
396 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400397
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700398 def to_json(self):
399 """Create a JSON representation of an instance of MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400400
401 Returns:
402 string, a JSON representation of this instance, suitable to pass to
403 from_json().
404 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700405 return self._to_json()
John Asmuth864311d2014-04-24 15:46:08 -0400406
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700407 @classmethod
408 def new_from_json(cls, s):
409 """Utility class method to instantiate a MediaUpload subclass from a JSON
John Asmuth864311d2014-04-24 15:46:08 -0400410 representation produced by to_json().
411
412 Args:
413 s: string, JSON from to_json().
414
415 Returns:
416 An instance of the subclass of MediaUpload that was serialized with
417 to_json().
418 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700419 data = json.loads(s)
420 # Find and call the right classmethod from_json() to restore the object.
421 module = data["_module"]
422 m = __import__(module, fromlist=module.split(".")[:-1])
423 kls = getattr(m, data["_class"])
424 from_json = getattr(kls, "from_json")
425 return from_json(s)
John Asmuth864311d2014-04-24 15:46:08 -0400426
427
428class MediaIoBaseUpload(MediaUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700429 """A MediaUpload for a io.Base objects.
John Asmuth864311d2014-04-24 15:46:08 -0400430
431 Note that the Python file object is compatible with io.Base and can be used
432 with this class also.
433
Pat Ferateed9affd2015-03-03 16:03:15 -0800434 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400435 media = MediaIoBaseUpload(fh, mimetype='image/png',
436 chunksize=1024*1024, resumable=True)
437 farm.animals().insert(
438 id='cow',
439 name='cow.png',
440 media_body=media).execute()
441
442 Depending on the platform you are working on, you may pass -1 as the
443 chunksize, which indicates that the entire file should be uploaded in a single
444 request. If the underlying platform supports streams, such as Python 2.6 or
445 later, then this can be very efficient as it avoids multiple connections, and
446 also avoids loading the entire file into memory before sending it. Note that
447 Google App Engine has a 5MB limit on request size, so you should never set
448 your chunksize larger than 5MB, or to -1.
449 """
450
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700451 @util.positional(3)
452 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
453 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400454
455 Args:
456 fd: io.Base or file object, The source of the bytes to upload. MUST be
457 opened in blocking mode, do not use streams opened in non-blocking mode.
458 The given stream must be seekable, that is, it must be able to call
459 seek() on fd.
460 mimetype: string, Mime-type of the file.
461 chunksize: int, File will be uploaded in chunks of this many bytes. Only
462 used if resumable=True. Pass in a value of -1 if the file is to be
463 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
464 on request size, so you should never set your chunksize larger than 5MB,
465 or to -1.
466 resumable: bool, True if this is a resumable upload. False means upload
467 in a single request.
468 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700469 super(MediaIoBaseUpload, self).__init__()
470 self._fd = fd
471 self._mimetype = mimetype
472 if not (chunksize == -1 or chunksize > 0):
473 raise InvalidChunkSizeError()
474 self._chunksize = chunksize
475 self._resumable = resumable
John Asmuth864311d2014-04-24 15:46:08 -0400476
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700477 self._fd.seek(0, os.SEEK_END)
478 self._size = self._fd.tell()
John Asmuth864311d2014-04-24 15:46:08 -0400479
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700480 def chunksize(self):
481 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400482
483 Returns:
484 Chunk size in bytes.
485 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700486 return self._chunksize
John Asmuth864311d2014-04-24 15:46:08 -0400487
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700488 def mimetype(self):
489 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400490
491 Returns:
492 Mime type.
493 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700494 return self._mimetype
John Asmuth864311d2014-04-24 15:46:08 -0400495
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700496 def size(self):
497 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400498
499 Returns:
500 Size of the body, or None of the size is unknown.
501 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700502 return self._size
John Asmuth864311d2014-04-24 15:46:08 -0400503
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700504 def resumable(self):
505 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400506
507 Returns:
508 True if resumable upload or False.
509 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700510 return self._resumable
John Asmuth864311d2014-04-24 15:46:08 -0400511
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700512 def getbytes(self, begin, length):
513 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400514
515 Args:
516 begin: int, offset from beginning of file.
517 length: int, number of bytes to read, starting at begin.
518
519 Returns:
520 A string of bytes read. May be shorted than length if EOF was reached
521 first.
522 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700523 self._fd.seek(begin)
524 return self._fd.read(length)
John Asmuth864311d2014-04-24 15:46:08 -0400525
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700526 def has_stream(self):
527 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400528
529 Streaming means it is an io.IOBase subclass that supports seek, i.e.
530 seekable() returns True.
531
532 Returns:
533 True if the call to stream() will return an instance of a seekable io.Base
534 subclass.
535 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700536 return True
John Asmuth864311d2014-04-24 15:46:08 -0400537
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700538 def stream(self):
539 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400540
541 Returns:
542 The returned value is an io.IOBase subclass that supports seek, i.e.
543 seekable() returns True.
544 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700545 return self._fd
John Asmuth864311d2014-04-24 15:46:08 -0400546
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700547 def to_json(self):
548 """This upload type is not serializable."""
549 raise NotImplementedError("MediaIoBaseUpload is not serializable.")
John Asmuth864311d2014-04-24 15:46:08 -0400550
551
552class MediaFileUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700553 """A MediaUpload for a file.
John Asmuth864311d2014-04-24 15:46:08 -0400554
555 Construct a MediaFileUpload and pass as the media_body parameter of the
556 method. For example, if we had a service that allowed uploading images:
557
John Asmuth864311d2014-04-24 15:46:08 -0400558 media = MediaFileUpload('cow.png', mimetype='image/png',
559 chunksize=1024*1024, resumable=True)
560 farm.animals().insert(
561 id='cow',
562 name='cow.png',
563 media_body=media).execute()
564
565 Depending on the platform you are working on, you may pass -1 as the
566 chunksize, which indicates that the entire file should be uploaded in a single
567 request. If the underlying platform supports streams, such as Python 2.6 or
568 later, then this can be very efficient as it avoids multiple connections, and
569 also avoids loading the entire file into memory before sending it. Note that
570 Google App Engine has a 5MB limit on request size, so you should never set
571 your chunksize larger than 5MB, or to -1.
572 """
573
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700574 @util.positional(2)
575 def __init__(
576 self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False
577 ):
578 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400579
580 Args:
581 filename: string, Name of the file.
582 mimetype: string, Mime-type of the file. If None then a mime-type will be
583 guessed from the file extension.
584 chunksize: int, File will be uploaded in chunks of this many bytes. Only
585 used if resumable=True. Pass in a value of -1 if the file is to be
586 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
587 on request size, so you should never set your chunksize larger than 5MB,
588 or to -1.
589 resumable: bool, True if this is a resumable upload. False means upload
590 in a single request.
591 """
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500592 self._fd = None
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700593 self._filename = filename
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500594 self._fd = open(self._filename, "rb")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700595 if mimetype is None:
596 # No mimetype provided, make a guess.
597 mimetype, _ = mimetypes.guess_type(filename)
598 if mimetype is None:
599 # Guess failed, use octet-stream.
600 mimetype = "application/octet-stream"
601 super(MediaFileUpload, self).__init__(
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500602 self._fd, mimetype, chunksize=chunksize, resumable=resumable
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700603 )
John Asmuth864311d2014-04-24 15:46:08 -0400604
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700605 def __del__(self):
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500606 if self._fd:
607 self._fd.close()
Xiaofei Wang20b67582019-07-17 11:16:53 -0700608
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700609 def to_json(self):
610 """Creating a JSON representation of an instance of MediaFileUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400611
612 Returns:
613 string, a JSON representation of this instance, suitable to pass to
614 from_json().
615 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700616 return self._to_json(strip=["_fd"])
John Asmuth864311d2014-04-24 15:46:08 -0400617
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700618 @staticmethod
619 def from_json(s):
620 d = json.loads(s)
621 return MediaFileUpload(
622 d["_filename"],
623 mimetype=d["_mimetype"],
624 chunksize=d["_chunksize"],
625 resumable=d["_resumable"],
626 )
John Asmuth864311d2014-04-24 15:46:08 -0400627
628
629class MediaInMemoryUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700630 """MediaUpload for a chunk of bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400631
632 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
633 the stream.
634 """
635
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700636 @util.positional(2)
637 def __init__(
638 self,
639 body,
640 mimetype="application/octet-stream",
641 chunksize=DEFAULT_CHUNK_SIZE,
642 resumable=False,
643 ):
644 """Create a new MediaInMemoryUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400645
646 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
647 the stream.
648
649 Args:
650 body: string, Bytes of body content.
651 mimetype: string, Mime-type of the file or default of
652 'application/octet-stream'.
653 chunksize: int, File will be uploaded in chunks of this many bytes. Only
654 used if resumable=True.
655 resumable: bool, True if this is a resumable upload. False means upload
656 in a single request.
657 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700658 fd = BytesIO(body)
659 super(MediaInMemoryUpload, self).__init__(
660 fd, mimetype, chunksize=chunksize, resumable=resumable
661 )
John Asmuth864311d2014-04-24 15:46:08 -0400662
663
664class MediaIoBaseDownload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700665 """"Download media resources.
John Asmuth864311d2014-04-24 15:46:08 -0400666
667 Note that the Python file object is compatible with io.Base and can be used
668 with this class also.
669
670
671 Example:
672 request = farms.animals().get_media(id='cow')
673 fh = io.FileIO('cow.png', mode='wb')
674 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
675
676 done = False
677 while done is False:
678 status, done = downloader.next_chunk()
679 if status:
680 print "Download %d%%." % int(status.progress() * 100)
681 print "Download Complete!"
682 """
683
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700684 @util.positional(3)
685 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
686 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400687
688 Args:
689 fd: io.Base or file object, The stream in which to write the downloaded
690 bytes.
691 request: googleapiclient.http.HttpRequest, the media request to perform in
692 chunks.
693 chunksize: int, File will be downloaded in chunks of this many bytes.
694 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700695 self._fd = fd
696 self._request = request
697 self._uri = request.uri
698 self._chunksize = chunksize
699 self._progress = 0
700 self._total_size = None
701 self._done = False
John Asmuth864311d2014-04-24 15:46:08 -0400702
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700703 # Stubs for testing.
704 self._sleep = time.sleep
705 self._rand = random.random
John Asmuth864311d2014-04-24 15:46:08 -0400706
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700707 self._headers = {}
708 for k, v in six.iteritems(request.headers):
709 # allow users to supply custom headers by setting them on the request
710 # but strip out the ones that are set by default on requests generated by
711 # API methods like Drive's files().get(fileId=...)
712 if not k.lower() in ("accept", "accept-encoding", "user-agent"):
713 self._headers[k] = v
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400714
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700715 @util.positional(1)
716 def next_chunk(self, num_retries=0):
717 """Get the next chunk of the download.
John Asmuth864311d2014-04-24 15:46:08 -0400718
719 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500720 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400721 exponential backoff. If all retries fail, the raised HttpError
722 represents the last request. If zero (default), we attempt the
723 request only once.
724
725 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700726 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400727 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100728 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400729
730 Raises:
731 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000732 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400733 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700734 headers = self._headers.copy()
735 headers["range"] = "bytes=%d-%d" % (
736 self._progress,
737 self._progress + self._chunksize,
738 )
739 http = self._request.http
John Asmuth864311d2014-04-24 15:46:08 -0400740
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700741 resp, content = _retry_request(
742 http,
743 num_retries,
744 "media download",
745 self._sleep,
746 self._rand,
747 self._uri,
748 "GET",
749 headers=headers,
750 )
John Asmuth864311d2014-04-24 15:46:08 -0400751
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700752 if resp.status in [200, 206]:
753 if "content-location" in resp and resp["content-location"] != self._uri:
754 self._uri = resp["content-location"]
755 self._progress += len(content)
756 self._fd.write(content)
John Asmuth864311d2014-04-24 15:46:08 -0400757
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700758 if "content-range" in resp:
759 content_range = resp["content-range"]
760 length = content_range.rsplit("/", 1)[1]
761 self._total_size = int(length)
762 elif "content-length" in resp:
763 self._total_size = int(resp["content-length"])
John Asmuth864311d2014-04-24 15:46:08 -0400764
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700765 if self._total_size is None or self._progress == self._total_size:
766 self._done = True
767 return MediaDownloadProgress(self._progress, self._total_size), self._done
Bu Sun Kim86d87882020-10-22 08:51:16 -0600768 elif resp.status == 416:
769 # 416 is Range Not Satisfiable
770 # This typically occurs with a zero byte file
771 content_range = resp["content-range"]
772 length = content_range.rsplit("/", 1)[1]
773 self._total_size = int(length)
774 if self._total_size == 0:
775 self._done = True
776 return MediaDownloadProgress(self._progress, self._total_size), self._done
777 raise HttpError(resp, content, uri=self._uri)
John Asmuth864311d2014-04-24 15:46:08 -0400778
779
780class _StreamSlice(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700781 """Truncated stream.
John Asmuth864311d2014-04-24 15:46:08 -0400782
783 Takes a stream and presents a stream that is a slice of the original stream.
784 This is used when uploading media in chunks. In later versions of Python a
785 stream can be passed to httplib in place of the string of data to send. The
786 problem is that httplib just blindly reads to the end of the stream. This
787 wrapper presents a virtual stream that only reads to the end of the chunk.
788 """
789
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700790 def __init__(self, stream, begin, chunksize):
791 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400792
793 Args:
794 stream: (io.Base, file object), the stream to wrap.
795 begin: int, the seek position the chunk begins at.
796 chunksize: int, the size of the chunk.
797 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700798 self._stream = stream
799 self._begin = begin
800 self._chunksize = chunksize
801 self._stream.seek(begin)
John Asmuth864311d2014-04-24 15:46:08 -0400802
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700803 def read(self, n=-1):
804 """Read n bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400805
806 Args:
807 n, int, the number of bytes to read.
808
809 Returns:
810 A string of length 'n', or less if EOF is reached.
811 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700812 # The data left available to read sits in [cur, end)
813 cur = self._stream.tell()
814 end = self._begin + self._chunksize
815 if n == -1 or cur + n > end:
816 n = end - cur
817 return self._stream.read(n)
John Asmuth864311d2014-04-24 15:46:08 -0400818
819
820class HttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700821 """Encapsulates a single HTTP request."""
John Asmuth864311d2014-04-24 15:46:08 -0400822
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700823 @util.positional(4)
824 def __init__(
825 self,
826 http,
827 postproc,
828 uri,
829 method="GET",
830 body=None,
831 headers=None,
832 methodId=None,
833 resumable=None,
834 ):
835 """Constructor for an HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -0400836
837 Args:
838 http: httplib2.Http, the transport object to use to make a request
839 postproc: callable, called on the HTTP response and content to transform
840 it into a data object before returning, or raising an exception
841 on an error.
842 uri: string, the absolute URI to send the request to
843 method: string, the HTTP method to use
844 body: string, the request body of the HTTP request,
845 headers: dict, the HTTP request headers
846 methodId: string, a unique identifier for the API method being called.
847 resumable: MediaUpload, None if this is not a resumbale request.
848 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700849 self.uri = uri
850 self.method = method
851 self.body = body
852 self.headers = headers or {}
853 self.methodId = methodId
854 self.http = http
855 self.postproc = postproc
856 self.resumable = resumable
857 self.response_callbacks = []
858 self._in_error_state = False
John Asmuth864311d2014-04-24 15:46:08 -0400859
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700860 # The size of the non-media part of the request.
861 self.body_size = len(self.body or "")
John Asmuth864311d2014-04-24 15:46:08 -0400862
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700863 # The resumable URI to send chunks to.
864 self.resumable_uri = None
John Asmuth864311d2014-04-24 15:46:08 -0400865
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700866 # The bytes that have been uploaded.
867 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -0400868
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700869 # Stubs for testing.
870 self._rand = random.random
871 self._sleep = time.sleep
John Asmuth864311d2014-04-24 15:46:08 -0400872
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700873 @util.positional(1)
874 def execute(self, http=None, num_retries=0):
875 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -0400876
877 Args:
878 http: httplib2.Http, an http object to be used in place of the
879 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500880 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400881 exponential backoff. If all retries fail, the raised HttpError
882 represents the last request. If zero (default), we attempt the
883 request only once.
884
885 Returns:
886 A deserialized object model of the response body as determined
887 by the postproc.
888
889 Raises:
890 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000891 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400892 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700893 if http is None:
894 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400895
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700896 if self.resumable:
897 body = None
898 while body is None:
899 _, body = self.next_chunk(http=http, num_retries=num_retries)
900 return body
John Asmuth864311d2014-04-24 15:46:08 -0400901
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700902 # Non-resumable case.
John Asmuth864311d2014-04-24 15:46:08 -0400903
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700904 if "content-length" not in self.headers:
905 self.headers["content-length"] = str(self.body_size)
906 # If the request URI is too long then turn it into a POST request.
907 # Assume that a GET request never contains a request body.
908 if len(self.uri) > MAX_URI_LENGTH and self.method == "GET":
909 self.method = "POST"
910 self.headers["x-http-method-override"] = "GET"
911 self.headers["content-type"] = "application/x-www-form-urlencoded"
912 parsed = urlparse(self.uri)
913 self.uri = urlunparse(
914 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
915 )
916 self.body = parsed.query
917 self.headers["content-length"] = str(len(self.body))
John Asmuth864311d2014-04-24 15:46:08 -0400918
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700919 # Handle retries for server-side errors.
920 resp, content = _retry_request(
921 http,
922 num_retries,
923 "request",
924 self._sleep,
925 self._rand,
926 str(self.uri),
927 method=str(self.method),
928 body=self.body,
929 headers=self.headers,
930 )
John Asmuth864311d2014-04-24 15:46:08 -0400931
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700932 for callback in self.response_callbacks:
933 callback(resp)
934 if resp.status >= 300:
935 raise HttpError(resp, content, uri=self.uri)
936 return self.postproc(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400937
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700938 @util.positional(2)
939 def add_response_callback(self, cb):
940 """add_response_headers_callback
John Asmuth864311d2014-04-24 15:46:08 -0400941
942 Args:
943 cb: Callback to be called on receiving the response headers, of signature:
944
945 def cb(resp):
946 # Where resp is an instance of httplib2.Response
947 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700948 self.response_callbacks.append(cb)
John Asmuth864311d2014-04-24 15:46:08 -0400949
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700950 @util.positional(1)
951 def next_chunk(self, http=None, num_retries=0):
952 """Execute the next step of a resumable upload.
John Asmuth864311d2014-04-24 15:46:08 -0400953
954 Can only be used if the method being executed supports media uploads and
955 the MediaUpload object passed in was flagged as using resumable upload.
956
957 Example:
958
959 media = MediaFileUpload('cow.png', mimetype='image/png',
960 chunksize=1000, resumable=True)
961 request = farm.animals().insert(
962 id='cow',
963 name='cow.png',
964 media_body=media)
965
966 response = None
967 while response is None:
968 status, response = request.next_chunk()
969 if status:
970 print "Upload %d%% complete." % int(status.progress() * 100)
971
972
973 Args:
974 http: httplib2.Http, an http object to be used in place of the
975 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500976 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400977 exponential backoff. If all retries fail, the raised HttpError
978 represents the last request. If zero (default), we attempt the
979 request only once.
980
981 Returns:
982 (status, body): (ResumableMediaStatus, object)
983 The body will be None until the resumable media is fully uploaded.
984
985 Raises:
986 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000987 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400988 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700989 if http is None:
990 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400991
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700992 if self.resumable.size() is None:
993 size = "*"
994 else:
995 size = str(self.resumable.size())
John Asmuth864311d2014-04-24 15:46:08 -0400996
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700997 if self.resumable_uri is None:
998 start_headers = copy.copy(self.headers)
999 start_headers["X-Upload-Content-Type"] = self.resumable.mimetype()
1000 if size != "*":
1001 start_headers["X-Upload-Content-Length"] = size
1002 start_headers["content-length"] = str(self.body_size)
John Asmuth864311d2014-04-24 15:46:08 -04001003
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001004 resp, content = _retry_request(
1005 http,
1006 num_retries,
1007 "resumable URI request",
1008 self._sleep,
1009 self._rand,
1010 self.uri,
1011 method=self.method,
1012 body=self.body,
1013 headers=start_headers,
1014 )
John Asmuth864311d2014-04-24 15:46:08 -04001015
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001016 if resp.status == 200 and "location" in resp:
1017 self.resumable_uri = resp["location"]
1018 else:
1019 raise ResumableUploadError(resp, content)
1020 elif self._in_error_state:
1021 # If we are in an error state then query the server for current state of
1022 # the upload by sending an empty PUT and reading the 'range' header in
1023 # the response.
1024 headers = {"Content-Range": "bytes */%s" % size, "content-length": "0"}
1025 resp, content = http.request(self.resumable_uri, "PUT", headers=headers)
1026 status, body = self._process_response(resp, content)
1027 if body:
1028 # The upload was complete.
1029 return (status, body)
John Asmuth864311d2014-04-24 15:46:08 -04001030
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001031 if self.resumable.has_stream():
1032 data = self.resumable.stream()
1033 if self.resumable.chunksize() == -1:
1034 data.seek(self.resumable_progress)
1035 chunk_end = self.resumable.size() - self.resumable_progress - 1
1036 else:
1037 # Doing chunking with a stream, so wrap a slice of the stream.
1038 data = _StreamSlice(
1039 data, self.resumable_progress, self.resumable.chunksize()
1040 )
1041 chunk_end = min(
1042 self.resumable_progress + self.resumable.chunksize() - 1,
1043 self.resumable.size() - 1,
1044 )
1045 else:
1046 data = self.resumable.getbytes(
1047 self.resumable_progress, self.resumable.chunksize()
1048 )
John Asmuth864311d2014-04-24 15:46:08 -04001049
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001050 # A short read implies that we are at EOF, so finish the upload.
1051 if len(data) < self.resumable.chunksize():
1052 size = str(self.resumable_progress + len(data))
John Asmuth864311d2014-04-24 15:46:08 -04001053
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001054 chunk_end = self.resumable_progress + len(data) - 1
John Asmuth864311d2014-04-24 15:46:08 -04001055
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001056 headers = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001057 # Must set the content-length header here because httplib can't
1058 # calculate the size when working with _StreamSlice.
1059 "Content-Length": str(chunk_end - self.resumable_progress + 1),
John Asmuth864311d2014-04-24 15:46:08 -04001060 }
1061
Bu Sun Kimaf6035f2020-10-20 16:36:04 -06001062 # An empty file results in chunk_end = -1 and size = 0
1063 # sending "bytes 0--1/0" results in an invalid request
1064 # Only add header "Content-Range" if chunk_end != -1
1065 if chunk_end != -1:
1066 headers["Content-Range"] = "bytes %d-%d/%s" % (self.resumable_progress, chunk_end, size)
1067
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001068 for retry_num in range(num_retries + 1):
1069 if retry_num > 0:
1070 self._sleep(self._rand() * 2 ** retry_num)
1071 LOGGER.warning(
1072 "Retry #%d for media upload: %s %s, following status: %d"
1073 % (retry_num, self.method, self.uri, resp.status)
1074 )
John Asmuth864311d2014-04-24 15:46:08 -04001075
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001076 try:
1077 resp, content = http.request(
1078 self.resumable_uri, method="PUT", body=data, headers=headers
1079 )
1080 except:
1081 self._in_error_state = True
1082 raise
1083 if not _should_retry_response(resp.status, content):
1084 break
John Asmuth864311d2014-04-24 15:46:08 -04001085
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001086 return self._process_response(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -04001087
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001088 def _process_response(self, resp, content):
1089 """Process the response from a single chunk upload.
John Asmuth864311d2014-04-24 15:46:08 -04001090
1091 Args:
1092 resp: httplib2.Response, the response object.
1093 content: string, the content of the response.
1094
1095 Returns:
1096 (status, body): (ResumableMediaStatus, object)
1097 The body will be None until the resumable media is fully uploaded.
1098
1099 Raises:
1100 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1101 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001102 if resp.status in [200, 201]:
1103 self._in_error_state = False
1104 return None, self.postproc(resp, content)
1105 elif resp.status == 308:
1106 self._in_error_state = False
1107 # A "308 Resume Incomplete" indicates we are not done.
1108 try:
1109 self.resumable_progress = int(resp["range"].split("-")[1]) + 1
1110 except KeyError:
1111 # If resp doesn't contain range header, resumable progress is 0
1112 self.resumable_progress = 0
1113 if "location" in resp:
1114 self.resumable_uri = resp["location"]
1115 else:
1116 self._in_error_state = True
1117 raise HttpError(resp, content, uri=self.uri)
John Asmuth864311d2014-04-24 15:46:08 -04001118
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001119 return (
1120 MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1121 None,
1122 )
John Asmuth864311d2014-04-24 15:46:08 -04001123
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001124 def to_json(self):
1125 """Returns a JSON representation of the HttpRequest."""
1126 d = copy.copy(self.__dict__)
1127 if d["resumable"] is not None:
1128 d["resumable"] = self.resumable.to_json()
1129 del d["http"]
1130 del d["postproc"]
1131 del d["_sleep"]
1132 del d["_rand"]
John Asmuth864311d2014-04-24 15:46:08 -04001133
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001134 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001135
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001136 @staticmethod
1137 def from_json(s, http, postproc):
1138 """Returns an HttpRequest populated with info from a JSON object."""
1139 d = json.loads(s)
1140 if d["resumable"] is not None:
1141 d["resumable"] = MediaUpload.new_from_json(d["resumable"])
1142 return HttpRequest(
1143 http,
1144 postproc,
1145 uri=d["uri"],
1146 method=d["method"],
1147 body=d["body"],
1148 headers=d["headers"],
1149 methodId=d["methodId"],
1150 resumable=d["resumable"],
1151 )
John Asmuth864311d2014-04-24 15:46:08 -04001152
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001153 @staticmethod
1154 def null_postproc(resp, contents):
1155 return resp, contents
1156
John Asmuth864311d2014-04-24 15:46:08 -04001157
1158class BatchHttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001159 """Batches multiple HttpRequest objects into a single HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001160
1161 Example:
1162 from googleapiclient.http import BatchHttpRequest
1163
1164 def list_animals(request_id, response, exception):
1165 \"\"\"Do something with the animals list response.\"\"\"
1166 if exception is not None:
1167 # Do something with the exception.
1168 pass
1169 else:
1170 # Do something with the response.
1171 pass
1172
1173 def list_farmers(request_id, response, exception):
1174 \"\"\"Do something with the farmers list response.\"\"\"
1175 if exception is not None:
1176 # Do something with the exception.
1177 pass
1178 else:
1179 # Do something with the response.
1180 pass
1181
1182 service = build('farm', 'v2')
1183
1184 batch = BatchHttpRequest()
1185
1186 batch.add(service.animals().list(), list_animals)
1187 batch.add(service.farmers().list(), list_farmers)
1188 batch.execute(http=http)
1189 """
1190
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001191 @util.positional(1)
1192 def __init__(self, callback=None, batch_uri=None):
1193 """Constructor for a BatchHttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001194
1195 Args:
1196 callback: callable, A callback to be called for each response, of the
1197 form callback(id, response, exception). The first parameter is the
1198 request id, and the second is the deserialized response object. The
1199 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1200 occurred while processing the request, or None if no error occurred.
1201 batch_uri: string, URI to send batch requests to.
1202 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001203 if batch_uri is None:
1204 batch_uri = _LEGACY_BATCH_URI
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001205
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001206 if batch_uri == _LEGACY_BATCH_URI:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001207 LOGGER.warning(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001208 "You have constructed a BatchHttpRequest using the legacy batch "
Brad Vogel6ddadd72020-05-15 10:02:04 -07001209 "endpoint %s. This endpoint will be turned down on August 12, 2020. "
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001210 "Please provide the API-specific endpoint or use "
1211 "service.new_batch_http_request(). For more details see "
1212 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1213 "and https://developers.google.com/api-client-library/python/guide/batch.",
1214 _LEGACY_BATCH_URI,
1215 )
1216 self._batch_uri = batch_uri
John Asmuth864311d2014-04-24 15:46:08 -04001217
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001218 # Global callback to be called for each individual response in the batch.
1219 self._callback = callback
John Asmuth864311d2014-04-24 15:46:08 -04001220
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001221 # A map from id to request.
1222 self._requests = {}
John Asmuth864311d2014-04-24 15:46:08 -04001223
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001224 # A map from id to callback.
1225 self._callbacks = {}
John Asmuth864311d2014-04-24 15:46:08 -04001226
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001227 # List of request ids, in the order in which they were added.
1228 self._order = []
John Asmuth864311d2014-04-24 15:46:08 -04001229
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001230 # The last auto generated id.
1231 self._last_auto_id = 0
John Asmuth864311d2014-04-24 15:46:08 -04001232
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001233 # Unique ID on which to base the Content-ID headers.
1234 self._base_id = None
John Asmuth864311d2014-04-24 15:46:08 -04001235
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001236 # A map from request id to (httplib2.Response, content) response pairs
1237 self._responses = {}
John Asmuth864311d2014-04-24 15:46:08 -04001238
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001239 # A map of id(Credentials) that have been refreshed.
1240 self._refreshed_credentials = {}
John Asmuth864311d2014-04-24 15:46:08 -04001241
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001242 def _refresh_and_apply_credentials(self, request, http):
1243 """Refresh the credentials and apply to the request.
John Asmuth864311d2014-04-24 15:46:08 -04001244
1245 Args:
1246 request: HttpRequest, the request.
1247 http: httplib2.Http, the global http object for the batch.
1248 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001249 # For the credentials to refresh, but only once per refresh_token
1250 # If there is no http per the request then refresh the http passed in
1251 # via execute()
1252 creds = None
1253 request_credentials = False
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001254
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001255 if request.http is not None:
1256 creds = _auth.get_credentials_from_http(request.http)
1257 request_credentials = True
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001258
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001259 if creds is None and http is not None:
1260 creds = _auth.get_credentials_from_http(http)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001261
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001262 if creds is not None:
1263 if id(creds) not in self._refreshed_credentials:
1264 _auth.refresh_credentials(creds)
1265 self._refreshed_credentials[id(creds)] = 1
John Asmuth864311d2014-04-24 15:46:08 -04001266
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001267 # Only apply the credentials if we are using the http object passed in,
1268 # otherwise apply() will get called during _serialize_request().
1269 if request.http is None or not request_credentials:
1270 _auth.apply_credentials(creds, request.headers)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001271
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001272 def _id_to_header(self, id_):
1273 """Convert an id to a Content-ID header value.
John Asmuth864311d2014-04-24 15:46:08 -04001274
1275 Args:
1276 id_: string, identifier of individual request.
1277
1278 Returns:
1279 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1280 the value because Content-ID headers are supposed to be universally
1281 unique.
1282 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001283 if self._base_id is None:
1284 self._base_id = uuid.uuid4()
John Asmuth864311d2014-04-24 15:46:08 -04001285
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001286 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1287 # line folding works properly on Python 3; see
Marie J.I48f503f2020-05-15 13:32:11 -04001288 # https://github.com/googleapis/google-api-python-client/issues/164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001289 return "<%s + %s>" % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001290
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001291 def _header_to_id(self, header):
1292 """Convert a Content-ID header value to an id.
John Asmuth864311d2014-04-24 15:46:08 -04001293
1294 Presumes the Content-ID header conforms to the format that _id_to_header()
1295 returns.
1296
1297 Args:
1298 header: string, Content-ID header value.
1299
1300 Returns:
1301 The extracted id value.
1302
1303 Raises:
1304 BatchError if the header is not in the expected format.
1305 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001306 if header[0] != "<" or header[-1] != ">":
1307 raise BatchError("Invalid value for Content-ID: %s" % header)
1308 if "+" not in header:
1309 raise BatchError("Invalid value for Content-ID: %s" % header)
1310 base, id_ = header[1:-1].split(" + ", 1)
John Asmuth864311d2014-04-24 15:46:08 -04001311
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001312 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001313
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001314 def _serialize_request(self, request):
1315 """Convert an HttpRequest object into a string.
John Asmuth864311d2014-04-24 15:46:08 -04001316
1317 Args:
1318 request: HttpRequest, the request to serialize.
1319
1320 Returns:
1321 The request as a string in application/http format.
1322 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001323 # Construct status line
1324 parsed = urlparse(request.uri)
1325 request_line = urlunparse(
1326 ("", "", parsed.path, parsed.params, parsed.query, "")
John Asmuth864311d2014-04-24 15:46:08 -04001327 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001328 status_line = request.method + " " + request_line + " HTTP/1.1\n"
1329 major, minor = request.headers.get("content-type", "application/json").split(
1330 "/"
1331 )
1332 msg = MIMENonMultipart(major, minor)
1333 headers = request.headers.copy()
John Asmuth864311d2014-04-24 15:46:08 -04001334
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001335 if request.http is not None:
1336 credentials = _auth.get_credentials_from_http(request.http)
1337 if credentials is not None:
1338 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001339
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001340 # MIMENonMultipart adds its own Content-Type header.
1341 if "content-type" in headers:
1342 del headers["content-type"]
John Asmuth864311d2014-04-24 15:46:08 -04001343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001344 for key, value in six.iteritems(headers):
1345 msg[key] = value
1346 msg["Host"] = parsed.netloc
1347 msg.set_unixfrom(None)
John Asmuth864311d2014-04-24 15:46:08 -04001348
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001349 if request.body is not None:
1350 msg.set_payload(request.body)
1351 msg["content-length"] = str(len(request.body))
John Asmuth864311d2014-04-24 15:46:08 -04001352
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001353 # Serialize the mime message.
1354 fp = StringIO()
1355 # maxheaderlen=0 means don't line wrap headers.
1356 g = Generator(fp, maxheaderlen=0)
1357 g.flatten(msg, unixfrom=False)
1358 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001359
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001360 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001361
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001362 def _deserialize_response(self, payload):
1363 """Convert string into httplib2 response and content.
John Asmuth864311d2014-04-24 15:46:08 -04001364
1365 Args:
1366 payload: string, headers and body as a string.
1367
1368 Returns:
1369 A pair (resp, content), such as would be returned from httplib2.request.
1370 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001371 # Strip off the status line
1372 status_line, payload = payload.split("\n", 1)
1373 protocol, status, reason = status_line.split(" ", 2)
John Asmuth864311d2014-04-24 15:46:08 -04001374
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001375 # Parse the rest of the response
1376 parser = FeedParser()
1377 parser.feed(payload)
1378 msg = parser.close()
1379 msg["status"] = status
John Asmuth864311d2014-04-24 15:46:08 -04001380
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001381 # Create httplib2.Response from the parsed headers.
1382 resp = httplib2.Response(msg)
1383 resp.reason = reason
1384 resp.version = int(protocol.split("/", 1)[1].replace(".", ""))
John Asmuth864311d2014-04-24 15:46:08 -04001385
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001386 content = payload.split("\r\n\r\n", 1)[1]
John Asmuth864311d2014-04-24 15:46:08 -04001387
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001388 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001389
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001390 def _new_id(self):
1391 """Create a new id.
John Asmuth864311d2014-04-24 15:46:08 -04001392
1393 Auto incrementing number that avoids conflicts with ids already used.
1394
1395 Returns:
1396 string, a new unique id.
1397 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001398 self._last_auto_id += 1
1399 while str(self._last_auto_id) in self._requests:
1400 self._last_auto_id += 1
1401 return str(self._last_auto_id)
John Asmuth864311d2014-04-24 15:46:08 -04001402
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001403 @util.positional(2)
1404 def add(self, request, callback=None, request_id=None):
1405 """Add a new request.
John Asmuth864311d2014-04-24 15:46:08 -04001406
1407 Every callback added will be paired with a unique id, the request_id. That
1408 unique id will be passed back to the callback when the response comes back
1409 from the server. The default behavior is to have the library generate it's
1410 own unique id. If the caller passes in a request_id then they must ensure
1411 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001412 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001413 request id, to avoid such an error.
1414
1415 Args:
1416 request: HttpRequest, Request to add to the batch.
1417 callback: callable, A callback to be called for this response, of the
1418 form callback(id, response, exception). The first parameter is the
1419 request id, and the second is the deserialized response object. The
1420 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1421 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001422 request_id: string, A unique id for the request. The id will be passed
1423 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001424
1425 Returns:
1426 None
1427
1428 Raises:
1429 BatchError if a media request is added to a batch.
1430 KeyError is the request_id is not unique.
1431 """
Xinan Line2dccec2018-12-07 05:28:33 +09001432
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001433 if len(self._order) >= MAX_BATCH_LIMIT:
1434 raise BatchError(
1435 "Exceeded the maximum calls(%d) in a single batch request."
1436 % MAX_BATCH_LIMIT
1437 )
1438 if request_id is None:
1439 request_id = self._new_id()
1440 if request.resumable is not None:
1441 raise BatchError("Media requests cannot be used in a batch request.")
1442 if request_id in self._requests:
1443 raise KeyError("A request with this ID already exists: %s" % request_id)
1444 self._requests[request_id] = request
1445 self._callbacks[request_id] = callback
1446 self._order.append(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001447
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001448 def _execute(self, http, order, requests):
1449 """Serialize batch request, send to server, process response.
John Asmuth864311d2014-04-24 15:46:08 -04001450
1451 Args:
1452 http: httplib2.Http, an http object to be used to make the request with.
1453 order: list, list of request ids in the order they were added to the
1454 batch.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001455 requests: list, list of request objects to send.
John Asmuth864311d2014-04-24 15:46:08 -04001456
1457 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001458 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001459 googleapiclient.errors.BatchError if the response is the wrong format.
1460 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001461 message = MIMEMultipart("mixed")
1462 # Message should not write out it's own headers.
1463 setattr(message, "_write_headers", lambda self: None)
John Asmuth864311d2014-04-24 15:46:08 -04001464
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001465 # Add all the individual requests.
1466 for request_id in order:
1467 request = requests[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001468
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001469 msg = MIMENonMultipart("application", "http")
1470 msg["Content-Transfer-Encoding"] = "binary"
1471 msg["Content-ID"] = self._id_to_header(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001472
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001473 body = self._serialize_request(request)
1474 msg.set_payload(body)
1475 message.attach(msg)
John Asmuth864311d2014-04-24 15:46:08 -04001476
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001477 # encode the body: note that we can't use `as_string`, because
1478 # it plays games with `From ` lines.
1479 fp = StringIO()
1480 g = Generator(fp, mangle_from_=False)
1481 g.flatten(message, unixfrom=False)
1482 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001483
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001484 headers = {}
1485 headers["content-type"] = (
1486 "multipart/mixed; " 'boundary="%s"'
1487 ) % message.get_boundary()
John Asmuth864311d2014-04-24 15:46:08 -04001488
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001489 resp, content = http.request(
1490 self._batch_uri, method="POST", body=body, headers=headers
1491 )
John Asmuth864311d2014-04-24 15:46:08 -04001492
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001493 if resp.status >= 300:
1494 raise HttpError(resp, content, uri=self._batch_uri)
John Asmuth864311d2014-04-24 15:46:08 -04001495
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001496 # Prepend with a content-type header so FeedParser can handle it.
1497 header = "content-type: %s\r\n\r\n" % resp["content-type"]
1498 # PY3's FeedParser only accepts unicode. So we should decode content
1499 # here, and encode each payload again.
1500 if six.PY3:
1501 content = content.decode("utf-8")
1502 for_parser = header + content
John Asmuth864311d2014-04-24 15:46:08 -04001503
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001504 parser = FeedParser()
1505 parser.feed(for_parser)
1506 mime_response = parser.close()
John Asmuth864311d2014-04-24 15:46:08 -04001507
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001508 if not mime_response.is_multipart():
1509 raise BatchError(
1510 "Response not in multipart/mixed format.", resp=resp, content=content
1511 )
John Asmuth864311d2014-04-24 15:46:08 -04001512
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001513 for part in mime_response.get_payload():
1514 request_id = self._header_to_id(part["Content-ID"])
1515 response, content = self._deserialize_response(part.get_payload())
1516 # We encode content here to emulate normal http response.
1517 if isinstance(content, six.text_type):
1518 content = content.encode("utf-8")
1519 self._responses[request_id] = (response, content)
John Asmuth864311d2014-04-24 15:46:08 -04001520
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001521 @util.positional(1)
1522 def execute(self, http=None):
1523 """Execute all the requests as a single batched HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001524
1525 Args:
1526 http: httplib2.Http, an http object to be used in place of the one the
1527 HttpRequest request object was constructed with. If one isn't supplied
1528 then use a http object from the requests in this batch.
1529
1530 Returns:
1531 None
1532
1533 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001534 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001535 googleapiclient.errors.BatchError if the response is the wrong format.
1536 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001537 # If we have no requests return
1538 if len(self._order) == 0:
1539 return None
John Asmuth864311d2014-04-24 15:46:08 -04001540
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001541 # If http is not supplied use the first valid one given in the requests.
1542 if http is None:
1543 for request_id in self._order:
1544 request = self._requests[request_id]
1545 if request is not None:
1546 http = request.http
1547 break
John Asmuth864311d2014-04-24 15:46:08 -04001548
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001549 if http is None:
1550 raise ValueError("Missing a valid http object.")
John Asmuth864311d2014-04-24 15:46:08 -04001551
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001552 # Special case for OAuth2Credentials-style objects which have not yet been
1553 # refreshed with an initial access_token.
1554 creds = _auth.get_credentials_from_http(http)
1555 if creds is not None:
1556 if not _auth.is_valid(creds):
1557 LOGGER.info("Attempting refresh to obtain initial access_token")
1558 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001559
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001560 self._execute(http, self._order, self._requests)
John Asmuth864311d2014-04-24 15:46:08 -04001561
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001562 # Loop over all the requests and check for 401s. For each 401 request the
1563 # credentials should be refreshed and then sent again in a separate batch.
1564 redo_requests = {}
1565 redo_order = []
John Asmuth864311d2014-04-24 15:46:08 -04001566
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001567 for request_id in self._order:
1568 resp, content = self._responses[request_id]
1569 if resp["status"] == "401":
1570 redo_order.append(request_id)
1571 request = self._requests[request_id]
1572 self._refresh_and_apply_credentials(request, http)
1573 redo_requests[request_id] = request
John Asmuth864311d2014-04-24 15:46:08 -04001574
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001575 if redo_requests:
1576 self._execute(http, redo_order, redo_requests)
John Asmuth864311d2014-04-24 15:46:08 -04001577
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001578 # Now process all callbacks that are erroring, and raise an exception for
1579 # ones that return a non-2xx response? Or add extra parameter to callback
1580 # that contains an HttpError?
John Asmuth864311d2014-04-24 15:46:08 -04001581
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001582 for request_id in self._order:
1583 resp, content = self._responses[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001584
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001585 request = self._requests[request_id]
1586 callback = self._callbacks[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001587
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001588 response = None
1589 exception = None
1590 try:
1591 if resp.status >= 300:
1592 raise HttpError(resp, content, uri=request.uri)
1593 response = request.postproc(resp, content)
1594 except HttpError as e:
1595 exception = e
John Asmuth864311d2014-04-24 15:46:08 -04001596
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001597 if callback is not None:
1598 callback(request_id, response, exception)
1599 if self._callback is not None:
1600 self._callback(request_id, response, exception)
John Asmuth864311d2014-04-24 15:46:08 -04001601
1602
1603class HttpRequestMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001604 """Mock of HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001605
1606 Do not construct directly, instead use RequestMockBuilder.
1607 """
1608
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001609 def __init__(self, resp, content, postproc):
1610 """Constructor for HttpRequestMock
John Asmuth864311d2014-04-24 15:46:08 -04001611
1612 Args:
1613 resp: httplib2.Response, the response to emulate coming from the request
1614 content: string, the response body
1615 postproc: callable, the post processing function usually supplied by
1616 the model class. See model.JsonModel.response() as an example.
1617 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001618 self.resp = resp
1619 self.content = content
1620 self.postproc = postproc
1621 if resp is None:
1622 self.resp = httplib2.Response({"status": 200, "reason": "OK"})
1623 if "reason" in self.resp:
1624 self.resp.reason = self.resp["reason"]
John Asmuth864311d2014-04-24 15:46:08 -04001625
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001626 def execute(self, http=None):
1627 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -04001628
1629 Same behavior as HttpRequest.execute(), but the response is
1630 mocked and not really from an HTTP request/response.
1631 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001632 return self.postproc(self.resp, self.content)
John Asmuth864311d2014-04-24 15:46:08 -04001633
1634
1635class RequestMockBuilder(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001636 """A simple mock of HttpRequest
John Asmuth864311d2014-04-24 15:46:08 -04001637
1638 Pass in a dictionary to the constructor that maps request methodIds to
1639 tuples of (httplib2.Response, content, opt_expected_body) that should be
1640 returned when that method is called. None may also be passed in for the
1641 httplib2.Response, in which case a 200 OK response will be generated.
1642 If an opt_expected_body (str or dict) is provided, it will be compared to
1643 the body and UnexpectedBodyError will be raised on inequality.
1644
1645 Example:
1646 response = '{"data": {"id": "tag:google.c...'
1647 requestBuilder = RequestMockBuilder(
1648 {
1649 'plus.activities.get': (None, response),
1650 }
1651 )
1652 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1653
1654 Methods that you do not supply a response for will return a
1655 200 OK with an empty string as the response content or raise an excpetion
1656 if check_unexpected is set to True. The methodId is taken from the rpcName
1657 in the discovery document.
1658
1659 For more details see the project wiki.
1660 """
1661
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001662 def __init__(self, responses, check_unexpected=False):
1663 """Constructor for RequestMockBuilder
John Asmuth864311d2014-04-24 15:46:08 -04001664
1665 The constructed object should be a callable object
1666 that can replace the class HttpResponse.
1667
1668 responses - A dictionary that maps methodIds into tuples
1669 of (httplib2.Response, content). The methodId
1670 comes from the 'rpcName' field in the discovery
1671 document.
1672 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1673 should be raised on unsupplied method.
1674 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001675 self.responses = responses
1676 self.check_unexpected = check_unexpected
John Asmuth864311d2014-04-24 15:46:08 -04001677
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001678 def __call__(
1679 self,
1680 http,
1681 postproc,
1682 uri,
1683 method="GET",
1684 body=None,
1685 headers=None,
1686 methodId=None,
1687 resumable=None,
1688 ):
1689 """Implements the callable interface that discovery.build() expects
John Asmuth864311d2014-04-24 15:46:08 -04001690 of requestBuilder, which is to build an object compatible with
1691 HttpRequest.execute(). See that method for the description of the
1692 parameters and the expected response.
1693 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001694 if methodId in self.responses:
1695 response = self.responses[methodId]
1696 resp, content = response[:2]
1697 if len(response) > 2:
1698 # Test the body against the supplied expected_body.
1699 expected_body = response[2]
1700 if bool(expected_body) != bool(body):
1701 # Not expecting a body and provided one
1702 # or expecting a body and not provided one.
1703 raise UnexpectedBodyError(expected_body, body)
1704 if isinstance(expected_body, str):
1705 expected_body = json.loads(expected_body)
1706 body = json.loads(body)
1707 if body != expected_body:
1708 raise UnexpectedBodyError(expected_body, body)
1709 return HttpRequestMock(resp, content, postproc)
1710 elif self.check_unexpected:
1711 raise UnexpectedMethodError(methodId=methodId)
1712 else:
1713 model = JsonModel(False)
1714 return HttpRequestMock(None, "{}", model.response)
John Asmuth864311d2014-04-24 15:46:08 -04001715
1716
1717class HttpMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001718 """Mock of httplib2.Http"""
John Asmuth864311d2014-04-24 15:46:08 -04001719
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001720 def __init__(self, filename=None, headers=None):
1721 """
John Asmuth864311d2014-04-24 15:46:08 -04001722 Args:
1723 filename: string, absolute filename to read response from
1724 headers: dict, header to return with response
1725 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001726 if headers is None:
1727 headers = {"status": "200"}
1728 if filename:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001729 with open(filename, "rb") as f:
1730 self.data = f.read()
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001731 else:
1732 self.data = None
1733 self.response_headers = headers
1734 self.headers = None
1735 self.uri = None
1736 self.method = None
1737 self.body = None
1738 self.headers = None
John Asmuth864311d2014-04-24 15:46:08 -04001739
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001740 def request(
1741 self,
1742 uri,
1743 method="GET",
1744 body=None,
1745 headers=None,
1746 redirections=1,
1747 connection_type=None,
1748 ):
1749 self.uri = uri
1750 self.method = method
1751 self.body = body
1752 self.headers = headers
1753 return httplib2.Response(self.response_headers), self.data
John Asmuth864311d2014-04-24 15:46:08 -04001754
Bu Sun Kim98888da2020-09-23 11:10:39 -06001755 def close(self):
1756 return None
John Asmuth864311d2014-04-24 15:46:08 -04001757
1758class HttpMockSequence(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001759 """Mock of httplib2.Http
John Asmuth864311d2014-04-24 15:46:08 -04001760
1761 Mocks a sequence of calls to request returning different responses for each
1762 call. Create an instance initialized with the desired response headers
1763 and content and then use as if an httplib2.Http instance.
1764
1765 http = HttpMockSequence([
1766 ({'status': '401'}, ''),
1767 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1768 ({'status': '200'}, 'echo_request_headers'),
1769 ])
1770 resp, content = http.request("http://examples.com")
1771
1772 There are special values you can pass in for content to trigger
1773 behavours that are helpful in testing.
1774
1775 'echo_request_headers' means return the request headers in the response body
1776 'echo_request_headers_as_json' means return the request headers in
1777 the response body
1778 'echo_request_body' means return the request body in the response body
1779 'echo_request_uri' means return the request uri in the response body
1780 """
1781
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001782 def __init__(self, iterable):
1783 """
John Asmuth864311d2014-04-24 15:46:08 -04001784 Args:
1785 iterable: iterable, a sequence of pairs of (headers, body)
1786 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001787 self._iterable = iterable
1788 self.follow_redirects = True
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001789 self.request_sequence = list()
John Asmuth864311d2014-04-24 15:46:08 -04001790
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001791 def request(
1792 self,
1793 uri,
1794 method="GET",
1795 body=None,
1796 headers=None,
1797 redirections=1,
1798 connection_type=None,
1799 ):
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001800 # Remember the request so after the fact this mock can be examined
1801 self.request_sequence.append((uri, method, body, headers))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001802 resp, content = self._iterable.pop(0)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001803 content = six.ensure_binary(content)
1804
1805 if content == b"echo_request_headers":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001806 content = headers
Matt McDonaldef6420a2020-04-14 16:28:13 -04001807 elif content == b"echo_request_headers_as_json":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001808 content = json.dumps(headers)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001809 elif content == b"echo_request_body":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001810 if hasattr(body, "read"):
1811 content = body.read()
1812 else:
1813 content = body
Matt McDonaldef6420a2020-04-14 16:28:13 -04001814 elif content == b"echo_request_uri":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001815 content = uri
1816 if isinstance(content, six.text_type):
1817 content = content.encode("utf-8")
1818 return httplib2.Response(resp), content
John Asmuth864311d2014-04-24 15:46:08 -04001819
1820
1821def set_user_agent(http, user_agent):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001822 """Set the user-agent on every request.
John Asmuth864311d2014-04-24 15:46:08 -04001823
1824 Args:
1825 http - An instance of httplib2.Http
1826 or something that acts like it.
1827 user_agent: string, the value for the user-agent header.
1828
1829 Returns:
1830 A modified instance of http that was passed in.
1831
1832 Example:
1833
1834 h = httplib2.Http()
1835 h = set_user_agent(h, "my-app-name/6.0")
1836
1837 Most of the time the user-agent will be set doing auth, this is for the rare
1838 cases where you are accessing an unauthenticated endpoint.
1839 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001840 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001841
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001842 # The closure that will replace 'httplib2.Http.request'.
1843 def new_request(
1844 uri,
1845 method="GET",
1846 body=None,
1847 headers=None,
1848 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1849 connection_type=None,
1850 ):
1851 """Modify the request headers to add the user-agent."""
1852 if headers is None:
1853 headers = {}
1854 if "user-agent" in headers:
1855 headers["user-agent"] = user_agent + " " + headers["user-agent"]
1856 else:
1857 headers["user-agent"] = user_agent
1858 resp, content = request_orig(
1859 uri,
1860 method=method,
1861 body=body,
1862 headers=headers,
1863 redirections=redirections,
1864 connection_type=connection_type,
1865 )
1866 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001867
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001868 http.request = new_request
1869 return http
John Asmuth864311d2014-04-24 15:46:08 -04001870
1871
1872def tunnel_patch(http):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001873 """Tunnel PATCH requests over POST.
John Asmuth864311d2014-04-24 15:46:08 -04001874 Args:
1875 http - An instance of httplib2.Http
1876 or something that acts like it.
1877
1878 Returns:
1879 A modified instance of http that was passed in.
1880
1881 Example:
1882
1883 h = httplib2.Http()
1884 h = tunnel_patch(h, "my-app-name/6.0")
1885
1886 Useful if you are running on a platform that doesn't support PATCH.
1887 Apply this last if you are using OAuth 1.0, as changing the method
1888 will result in a different signature.
1889 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001890 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001891
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001892 # The closure that will replace 'httplib2.Http.request'.
1893 def new_request(
1894 uri,
1895 method="GET",
1896 body=None,
1897 headers=None,
1898 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1899 connection_type=None,
1900 ):
1901 """Modify the request headers to add the user-agent."""
1902 if headers is None:
1903 headers = {}
1904 if method == "PATCH":
1905 if "oauth_token" in headers.get("authorization", ""):
1906 LOGGER.warning(
1907 "OAuth 1.0 request made with Credentials after tunnel_patch."
1908 )
1909 headers["x-http-method-override"] = "PATCH"
1910 method = "POST"
1911 resp, content = request_orig(
1912 uri,
1913 method=method,
1914 body=body,
1915 headers=headers,
1916 redirections=redirections,
1917 connection_type=connection_type,
1918 )
1919 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001920
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001921 http.request = new_request
1922 return http
Igor Maravić22435292017-01-19 22:28:22 +01001923
1924
1925def build_http():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001926 """Builds httplib2.Http object
Igor Maravić22435292017-01-19 22:28:22 +01001927
1928 Returns:
1929 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1930 To override default timeout call
1931
1932 socket.setdefaulttimeout(timeout_in_sec)
1933
1934 before interacting with this method.
1935 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001936 if socket.getdefaulttimeout() is not None:
1937 http_timeout = socket.getdefaulttimeout()
1938 else:
1939 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001940 http = httplib2.Http(timeout=http_timeout)
1941 # 308's are used by several Google APIs (Drive, YouTube)
1942 # for Resumable Uploads rather than Permanent Redirects.
1943 # This asks httplib2 to exclude 308s from the status codes
1944 # it treats as redirects
Bu Sun Kima480d532020-03-13 12:52:22 -07001945 try:
1946 http.redirect_codes = http.redirect_codes - {308}
1947 except AttributeError:
1948 # Apache Beam tests depend on this library and cannot
1949 # currently upgrade their httplib2 version
1950 # http.redirect_codes does not exist in previous versions
1951 # of httplib2, so pass
1952 pass
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001953
1954 return http