blob: e78f70f1ab9de7677edc08f7aab520ef190c0ffa [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 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070097 # Retry on 5xx errors.
98 if resp_status >= 500:
99 return True
eesheeshc6425a02016-02-12 15:07:06 +0000100
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700101 # Retry on 429 errors.
102 if resp_status == _TOO_MANY_REQUESTS:
103 return True
eesheeshc6425a02016-02-12 15:07:06 +0000104
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700105 # For 403 errors, we have to check for the `reason` in the response to
106 # determine if we should retry.
107 if resp_status == six.moves.http_client.FORBIDDEN:
108 # If there's no details about the 403 type, don't retry.
109 if not content:
110 return False
eesheeshc6425a02016-02-12 15:07:06 +0000111
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700112 # Content is in JSON format.
113 try:
114 data = json.loads(content.decode("utf-8"))
115 if isinstance(data, dict):
Kapil Thangaveluc6912832020-12-02 14:52:02 -0500116 reason = data["error"].get("status")
117 if reason is None:
118 reason = data["error"]["errors"][0]["reason"]
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700119 else:
120 reason = data[0]["error"]["errors"]["reason"]
121 except (UnicodeDecodeError, ValueError, KeyError):
122 LOGGER.warning("Invalid JSON content from response: %s", content)
123 return False
eesheeshc6425a02016-02-12 15:07:06 +0000124
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700125 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
eesheeshc6425a02016-02-12 15:07:06 +0000126
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700127 # Only retry on rate limit related failures.
128 if reason in ("userRateLimitExceeded", "rateLimitExceeded"):
129 return True
eesheeshc6425a02016-02-12 15:07:06 +0000130
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700131 # Everything else is a success or non-retriable so break.
132 return False
eesheeshc6425a02016-02-12 15:07:06 +0000133
John Asmuth864311d2014-04-24 15:46:08 -0400134
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700135def _retry_request(
136 http, num_retries, req_type, sleep, rand, uri, method, *args, **kwargs
137):
138 """Retries an HTTP request multiple times while handling errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100139
140 If after all retries the request still fails, last error is either returned as
141 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
142
143 Args:
144 http: Http object to be used to execute request.
145 num_retries: Maximum number of retries.
146 req_type: Type of the request (used for logging retries).
147 sleep, rand: Functions to sleep for random time between retries.
148 uri: URI to be requested.
149 method: HTTP method to be used.
150 args, kwargs: Additional arguments passed to http.request.
151
152 Returns:
153 resp, content - Response from the http request (may be HTTP 5xx).
154 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700155 resp = None
156 content = None
157 exception = None
158 for retry_num in range(num_retries + 1):
159 if retry_num > 0:
160 # Sleep before retrying.
161 sleep_time = rand() * 2 ** retry_num
162 LOGGER.warning(
163 "Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s",
164 sleep_time,
165 retry_num,
166 num_retries,
167 req_type,
168 method,
169 uri,
170 resp.status if resp else exception,
171 )
172 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100173
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700174 try:
175 exception = None
176 resp, content = http.request(uri, method, *args, **kwargs)
177 # Retry on SSL errors and socket timeout errors.
178 except _ssl_SSLError as ssl_error:
179 exception = ssl_error
180 except socket.timeout as socket_timeout:
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600181 # Needs to be before socket.error as it's a subclass of OSError
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700182 # socket.timeout has no errorcode
183 exception = socket_timeout
Damian Gadomskic7516a22020-03-23 20:39:21 +0100184 except ConnectionError as connection_error:
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600185 # Needs to be before socket.error as it's a subclass of OSError
Damian Gadomskic7516a22020-03-23 20:39:21 +0100186 exception = connection_error
Aaron Niskode-Dossettb7b99862021-01-13 09:18:01 -0600187 except OSError as socket_error:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700188 # errno's contents differ by platform, so we have to match by name.
Aaron Niskode-Dossettae9cd992021-01-13 04:58:15 -0600189 # Some of these same errors may have been caught above, e.g. ECONNRESET *should* be
190 # raised as a ConnectionError, but some libraries will raise it as a socket.error
191 # with an errno corresponding to ECONNRESET
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700192 if socket.errno.errorcode.get(socket_error.errno) not in {
193 "WSAETIMEDOUT",
194 "ETIMEDOUT",
195 "EPIPE",
196 "ECONNABORTED",
Aaron Niskode-Dossettae9cd992021-01-13 04:58:15 -0600197 "ECONNREFUSED",
198 "ECONNRESET",
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700199 }:
200 raise
201 exception = socket_error
202 except httplib2.ServerNotFoundError as server_not_found_error:
203 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000204
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700205 if exception:
206 if retry_num == num_retries:
207 raise exception
208 else:
209 continue
eesheeshc6425a02016-02-12 15:07:06 +0000210
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700211 if not _should_retry_response(resp.status, content):
212 break
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100213
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700214 return resp, content
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100215
216
John Asmuth864311d2014-04-24 15:46:08 -0400217class MediaUploadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700218 """Status of a resumable upload."""
John Asmuth864311d2014-04-24 15:46:08 -0400219
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700220 def __init__(self, resumable_progress, total_size):
221 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400222
223 Args:
224 resumable_progress: int, bytes sent so far.
225 total_size: int, total bytes in complete upload, or None if the total
226 upload size isn't known ahead of time.
227 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700228 self.resumable_progress = resumable_progress
229 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400230
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700231 def progress(self):
232 """Percent of upload completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400233
234 Returns:
235 the percentage complete as a float, returning 0.0 if the total size of
236 the upload is unknown.
237 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700238 if self.total_size is not None and self.total_size != 0:
239 return float(self.resumable_progress) / float(self.total_size)
240 else:
241 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400242
243
244class MediaDownloadProgress(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700245 """Status of a resumable download."""
John Asmuth864311d2014-04-24 15:46:08 -0400246
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700247 def __init__(self, resumable_progress, total_size):
248 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400249
250 Args:
251 resumable_progress: int, bytes received so far.
252 total_size: int, total bytes in complete download.
253 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700254 self.resumable_progress = resumable_progress
255 self.total_size = total_size
John Asmuth864311d2014-04-24 15:46:08 -0400256
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700257 def progress(self):
258 """Percent of download completed, as a float.
John Asmuth864311d2014-04-24 15:46:08 -0400259
260 Returns:
261 the percentage complete as a float, returning 0.0 if the total size of
262 the download is unknown.
263 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700264 if self.total_size is not None and self.total_size != 0:
265 return float(self.resumable_progress) / float(self.total_size)
266 else:
267 return 0.0
John Asmuth864311d2014-04-24 15:46:08 -0400268
269
270class MediaUpload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700271 """Describes a media object to upload.
John Asmuth864311d2014-04-24 15:46:08 -0400272
273 Base class that defines the interface of MediaUpload subclasses.
274
275 Note that subclasses of MediaUpload may allow you to control the chunksize
276 when uploading a media object. It is important to keep the size of the chunk
277 as large as possible to keep the upload efficient. Other factors may influence
278 the size of the chunk you use, particularly if you are working in an
279 environment where individual HTTP requests may have a hardcoded time limit,
280 such as under certain classes of requests under Google App Engine.
281
282 Streams are io.Base compatible objects that support seek(). Some MediaUpload
283 subclasses support using streams directly to upload data. Support for
284 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
285 platform that stream will be used for uploading the media object. The support
286 for streaming is indicated by has_stream() returning True. The stream() method
287 should return an io.Base object that supports seek(). On platforms where the
288 underlying httplib module supports streaming, for example Python 2.6 and
289 later, the stream will be passed into the http library which will result in
290 less memory being used and possibly faster uploads.
291
292 If you need to upload media that can't be uploaded using any of the existing
293 MediaUpload sub-class then you can sub-class MediaUpload for your particular
294 needs.
295 """
296
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700297 def chunksize(self):
298 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400299
300 Returns:
301 Chunk size in bytes.
302 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700303 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400304
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700305 def mimetype(self):
306 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400307
308 Returns:
309 Mime type.
310 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700311 return "application/octet-stream"
John Asmuth864311d2014-04-24 15:46:08 -0400312
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700313 def size(self):
314 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400315
316 Returns:
317 Size of the body, or None of the size is unknown.
318 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700319 return None
John Asmuth864311d2014-04-24 15:46:08 -0400320
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700321 def resumable(self):
322 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400323
324 Returns:
325 True if resumable upload or False.
326 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700327 return False
John Asmuth864311d2014-04-24 15:46:08 -0400328
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700329 def getbytes(self, begin, end):
330 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400331
332 Args:
333 begin: int, offset from beginning of file.
334 length: int, number of bytes to read, starting at begin.
335
336 Returns:
337 A string of bytes read. May be shorter than length if EOF was reached
338 first.
339 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700340 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400341
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700342 def has_stream(self):
343 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400344
345 Streaming means it is an io.IOBase subclass that supports seek, i.e.
346 seekable() returns True.
347
348 Returns:
349 True if the call to stream() will return an instance of a seekable io.Base
350 subclass.
351 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700352 return False
John Asmuth864311d2014-04-24 15:46:08 -0400353
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700354 def stream(self):
355 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400356
357 Returns:
358 The returned value is an io.IOBase subclass that supports seek, i.e.
359 seekable() returns True.
360 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700361 raise NotImplementedError()
John Asmuth864311d2014-04-24 15:46:08 -0400362
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700363 @util.positional(1)
364 def _to_json(self, strip=None):
365 """Utility function for creating a JSON representation of a MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400366
367 Args:
368 strip: array, An array of names of members to not include in the JSON.
369
370 Returns:
371 string, a JSON representation of this instance, suitable to pass to
372 from_json().
373 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700374 t = type(self)
375 d = copy.copy(self.__dict__)
376 if strip is not None:
377 for member in strip:
378 del d[member]
379 d["_class"] = t.__name__
380 d["_module"] = t.__module__
381 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400382
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700383 def to_json(self):
384 """Create a JSON representation of an instance of MediaUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400385
386 Returns:
387 string, a JSON representation of this instance, suitable to pass to
388 from_json().
389 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700390 return self._to_json()
John Asmuth864311d2014-04-24 15:46:08 -0400391
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700392 @classmethod
393 def new_from_json(cls, s):
394 """Utility class method to instantiate a MediaUpload subclass from a JSON
John Asmuth864311d2014-04-24 15:46:08 -0400395 representation produced by to_json().
396
397 Args:
398 s: string, JSON from to_json().
399
400 Returns:
401 An instance of the subclass of MediaUpload that was serialized with
402 to_json().
403 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700404 data = json.loads(s)
405 # Find and call the right classmethod from_json() to restore the object.
406 module = data["_module"]
407 m = __import__(module, fromlist=module.split(".")[:-1])
408 kls = getattr(m, data["_class"])
409 from_json = getattr(kls, "from_json")
410 return from_json(s)
John Asmuth864311d2014-04-24 15:46:08 -0400411
412
413class MediaIoBaseUpload(MediaUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700414 """A MediaUpload for a io.Base objects.
John Asmuth864311d2014-04-24 15:46:08 -0400415
416 Note that the Python file object is compatible with io.Base and can be used
417 with this class also.
418
Pat Ferateed9affd2015-03-03 16:03:15 -0800419 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400420 media = MediaIoBaseUpload(fh, mimetype='image/png',
421 chunksize=1024*1024, resumable=True)
422 farm.animals().insert(
423 id='cow',
424 name='cow.png',
425 media_body=media).execute()
426
427 Depending on the platform you are working on, you may pass -1 as the
428 chunksize, which indicates that the entire file should be uploaded in a single
429 request. If the underlying platform supports streams, such as Python 2.6 or
430 later, then this can be very efficient as it avoids multiple connections, and
431 also avoids loading the entire file into memory before sending it. Note that
432 Google App Engine has a 5MB limit on request size, so you should never set
433 your chunksize larger than 5MB, or to -1.
434 """
435
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700436 @util.positional(3)
437 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
438 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400439
440 Args:
441 fd: io.Base or file object, The source of the bytes to upload. MUST be
442 opened in blocking mode, do not use streams opened in non-blocking mode.
443 The given stream must be seekable, that is, it must be able to call
444 seek() on fd.
445 mimetype: string, Mime-type of the file.
446 chunksize: int, File will be uploaded in chunks of this many bytes. Only
447 used if resumable=True. Pass in a value of -1 if the file is to be
448 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
449 on request size, so you should never set your chunksize larger than 5MB,
450 or to -1.
451 resumable: bool, True if this is a resumable upload. False means upload
452 in a single request.
453 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700454 super(MediaIoBaseUpload, self).__init__()
455 self._fd = fd
456 self._mimetype = mimetype
457 if not (chunksize == -1 or chunksize > 0):
458 raise InvalidChunkSizeError()
459 self._chunksize = chunksize
460 self._resumable = resumable
John Asmuth864311d2014-04-24 15:46:08 -0400461
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700462 self._fd.seek(0, os.SEEK_END)
463 self._size = self._fd.tell()
John Asmuth864311d2014-04-24 15:46:08 -0400464
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700465 def chunksize(self):
466 """Chunk size for resumable uploads.
John Asmuth864311d2014-04-24 15:46:08 -0400467
468 Returns:
469 Chunk size in bytes.
470 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700471 return self._chunksize
John Asmuth864311d2014-04-24 15:46:08 -0400472
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700473 def mimetype(self):
474 """Mime type of the body.
John Asmuth864311d2014-04-24 15:46:08 -0400475
476 Returns:
477 Mime type.
478 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700479 return self._mimetype
John Asmuth864311d2014-04-24 15:46:08 -0400480
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700481 def size(self):
482 """Size of upload.
John Asmuth864311d2014-04-24 15:46:08 -0400483
484 Returns:
485 Size of the body, or None of the size is unknown.
486 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700487 return self._size
John Asmuth864311d2014-04-24 15:46:08 -0400488
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700489 def resumable(self):
490 """Whether this upload is resumable.
John Asmuth864311d2014-04-24 15:46:08 -0400491
492 Returns:
493 True if resumable upload or False.
494 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700495 return self._resumable
John Asmuth864311d2014-04-24 15:46:08 -0400496
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700497 def getbytes(self, begin, length):
498 """Get bytes from the media.
John Asmuth864311d2014-04-24 15:46:08 -0400499
500 Args:
501 begin: int, offset from beginning of file.
502 length: int, number of bytes to read, starting at begin.
503
504 Returns:
505 A string of bytes read. May be shorted than length if EOF was reached
506 first.
507 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700508 self._fd.seek(begin)
509 return self._fd.read(length)
John Asmuth864311d2014-04-24 15:46:08 -0400510
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700511 def has_stream(self):
512 """Does the underlying upload support a streaming interface.
John Asmuth864311d2014-04-24 15:46:08 -0400513
514 Streaming means it is an io.IOBase subclass that supports seek, i.e.
515 seekable() returns True.
516
517 Returns:
518 True if the call to stream() will return an instance of a seekable io.Base
519 subclass.
520 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700521 return True
John Asmuth864311d2014-04-24 15:46:08 -0400522
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700523 def stream(self):
524 """A stream interface to the data being uploaded.
John Asmuth864311d2014-04-24 15:46:08 -0400525
526 Returns:
527 The returned value is an io.IOBase subclass that supports seek, i.e.
528 seekable() returns True.
529 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700530 return self._fd
John Asmuth864311d2014-04-24 15:46:08 -0400531
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700532 def to_json(self):
533 """This upload type is not serializable."""
534 raise NotImplementedError("MediaIoBaseUpload is not serializable.")
John Asmuth864311d2014-04-24 15:46:08 -0400535
536
537class MediaFileUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700538 """A MediaUpload for a file.
John Asmuth864311d2014-04-24 15:46:08 -0400539
540 Construct a MediaFileUpload and pass as the media_body parameter of the
541 method. For example, if we had a service that allowed uploading images:
542
John Asmuth864311d2014-04-24 15:46:08 -0400543 media = MediaFileUpload('cow.png', mimetype='image/png',
544 chunksize=1024*1024, resumable=True)
545 farm.animals().insert(
546 id='cow',
547 name='cow.png',
548 media_body=media).execute()
549
550 Depending on the platform you are working on, you may pass -1 as the
551 chunksize, which indicates that the entire file should be uploaded in a single
552 request. If the underlying platform supports streams, such as Python 2.6 or
553 later, then this can be very efficient as it avoids multiple connections, and
554 also avoids loading the entire file into memory before sending it. Note that
555 Google App Engine has a 5MB limit on request size, so you should never set
556 your chunksize larger than 5MB, or to -1.
557 """
558
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700559 @util.positional(2)
560 def __init__(
561 self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False
562 ):
563 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400564
565 Args:
566 filename: string, Name of the file.
567 mimetype: string, Mime-type of the file. If None then a mime-type will be
568 guessed from the file extension.
569 chunksize: int, File will be uploaded in chunks of this many bytes. Only
570 used if resumable=True. Pass in a value of -1 if the file is to be
571 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
572 on request size, so you should never set your chunksize larger than 5MB,
573 or to -1.
574 resumable: bool, True if this is a resumable upload. False means upload
575 in a single request.
576 """
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500577 self._fd = None
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700578 self._filename = filename
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500579 self._fd = open(self._filename, "rb")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700580 if mimetype is None:
581 # No mimetype provided, make a guess.
582 mimetype, _ = mimetypes.guess_type(filename)
583 if mimetype is None:
584 # Guess failed, use octet-stream.
585 mimetype = "application/octet-stream"
586 super(MediaFileUpload, self).__init__(
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500587 self._fd, mimetype, chunksize=chunksize, resumable=resumable
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700588 )
John Asmuth864311d2014-04-24 15:46:08 -0400589
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700590 def __del__(self):
Anthonios Partheniou2c6d0292020-12-09 18:56:01 -0500591 if self._fd:
592 self._fd.close()
Xiaofei Wang20b67582019-07-17 11:16:53 -0700593
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700594 def to_json(self):
595 """Creating a JSON representation of an instance of MediaFileUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400596
597 Returns:
598 string, a JSON representation of this instance, suitable to pass to
599 from_json().
600 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700601 return self._to_json(strip=["_fd"])
John Asmuth864311d2014-04-24 15:46:08 -0400602
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700603 @staticmethod
604 def from_json(s):
605 d = json.loads(s)
606 return MediaFileUpload(
607 d["_filename"],
608 mimetype=d["_mimetype"],
609 chunksize=d["_chunksize"],
610 resumable=d["_resumable"],
611 )
John Asmuth864311d2014-04-24 15:46:08 -0400612
613
614class MediaInMemoryUpload(MediaIoBaseUpload):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700615 """MediaUpload for a chunk of bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400616
617 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
618 the stream.
619 """
620
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700621 @util.positional(2)
622 def __init__(
623 self,
624 body,
625 mimetype="application/octet-stream",
626 chunksize=DEFAULT_CHUNK_SIZE,
627 resumable=False,
628 ):
629 """Create a new MediaInMemoryUpload.
John Asmuth864311d2014-04-24 15:46:08 -0400630
631 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
632 the stream.
633
634 Args:
635 body: string, Bytes of body content.
636 mimetype: string, Mime-type of the file or default of
637 'application/octet-stream'.
638 chunksize: int, File will be uploaded in chunks of this many bytes. Only
639 used if resumable=True.
640 resumable: bool, True if this is a resumable upload. False means upload
641 in a single request.
642 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700643 fd = BytesIO(body)
644 super(MediaInMemoryUpload, self).__init__(
645 fd, mimetype, chunksize=chunksize, resumable=resumable
646 )
John Asmuth864311d2014-04-24 15:46:08 -0400647
648
649class MediaIoBaseDownload(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700650 """"Download media resources.
John Asmuth864311d2014-04-24 15:46:08 -0400651
652 Note that the Python file object is compatible with io.Base and can be used
653 with this class also.
654
655
656 Example:
657 request = farms.animals().get_media(id='cow')
658 fh = io.FileIO('cow.png', mode='wb')
659 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
660
661 done = False
662 while done is False:
663 status, done = downloader.next_chunk()
664 if status:
665 print "Download %d%%." % int(status.progress() * 100)
666 print "Download Complete!"
667 """
668
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700669 @util.positional(3)
670 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
671 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400672
673 Args:
674 fd: io.Base or file object, The stream in which to write the downloaded
675 bytes.
676 request: googleapiclient.http.HttpRequest, the media request to perform in
677 chunks.
678 chunksize: int, File will be downloaded in chunks of this many bytes.
679 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700680 self._fd = fd
681 self._request = request
682 self._uri = request.uri
683 self._chunksize = chunksize
684 self._progress = 0
685 self._total_size = None
686 self._done = False
John Asmuth864311d2014-04-24 15:46:08 -0400687
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700688 # Stubs for testing.
689 self._sleep = time.sleep
690 self._rand = random.random
John Asmuth864311d2014-04-24 15:46:08 -0400691
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700692 self._headers = {}
693 for k, v in six.iteritems(request.headers):
694 # allow users to supply custom headers by setting them on the request
695 # but strip out the ones that are set by default on requests generated by
696 # API methods like Drive's files().get(fileId=...)
697 if not k.lower() in ("accept", "accept-encoding", "user-agent"):
698 self._headers[k] = v
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400699
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700700 @util.positional(1)
701 def next_chunk(self, num_retries=0):
702 """Get the next chunk of the download.
John Asmuth864311d2014-04-24 15:46:08 -0400703
704 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500705 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400706 exponential backoff. If all retries fail, the raised HttpError
707 represents the last request. If zero (default), we attempt the
708 request only once.
709
710 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700711 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400712 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100713 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400714
715 Raises:
716 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000717 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400718 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700719 headers = self._headers.copy()
720 headers["range"] = "bytes=%d-%d" % (
721 self._progress,
722 self._progress + self._chunksize,
723 )
724 http = self._request.http
John Asmuth864311d2014-04-24 15:46:08 -0400725
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700726 resp, content = _retry_request(
727 http,
728 num_retries,
729 "media download",
730 self._sleep,
731 self._rand,
732 self._uri,
733 "GET",
734 headers=headers,
735 )
John Asmuth864311d2014-04-24 15:46:08 -0400736
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700737 if resp.status in [200, 206]:
738 if "content-location" in resp and resp["content-location"] != self._uri:
739 self._uri = resp["content-location"]
740 self._progress += len(content)
741 self._fd.write(content)
John Asmuth864311d2014-04-24 15:46:08 -0400742
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700743 if "content-range" in resp:
744 content_range = resp["content-range"]
745 length = content_range.rsplit("/", 1)[1]
746 self._total_size = int(length)
747 elif "content-length" in resp:
748 self._total_size = int(resp["content-length"])
John Asmuth864311d2014-04-24 15:46:08 -0400749
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700750 if self._total_size is None or self._progress == self._total_size:
751 self._done = True
752 return MediaDownloadProgress(self._progress, self._total_size), self._done
Bu Sun Kim86d87882020-10-22 08:51:16 -0600753 elif resp.status == 416:
754 # 416 is Range Not Satisfiable
755 # This typically occurs with a zero byte file
756 content_range = resp["content-range"]
757 length = content_range.rsplit("/", 1)[1]
758 self._total_size = int(length)
759 if self._total_size == 0:
760 self._done = True
761 return MediaDownloadProgress(self._progress, self._total_size), self._done
762 raise HttpError(resp, content, uri=self._uri)
John Asmuth864311d2014-04-24 15:46:08 -0400763
764
765class _StreamSlice(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700766 """Truncated stream.
John Asmuth864311d2014-04-24 15:46:08 -0400767
768 Takes a stream and presents a stream that is a slice of the original stream.
769 This is used when uploading media in chunks. In later versions of Python a
770 stream can be passed to httplib in place of the string of data to send. The
771 problem is that httplib just blindly reads to the end of the stream. This
772 wrapper presents a virtual stream that only reads to the end of the chunk.
773 """
774
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700775 def __init__(self, stream, begin, chunksize):
776 """Constructor.
John Asmuth864311d2014-04-24 15:46:08 -0400777
778 Args:
779 stream: (io.Base, file object), the stream to wrap.
780 begin: int, the seek position the chunk begins at.
781 chunksize: int, the size of the chunk.
782 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700783 self._stream = stream
784 self._begin = begin
785 self._chunksize = chunksize
786 self._stream.seek(begin)
John Asmuth864311d2014-04-24 15:46:08 -0400787
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700788 def read(self, n=-1):
789 """Read n bytes.
John Asmuth864311d2014-04-24 15:46:08 -0400790
791 Args:
792 n, int, the number of bytes to read.
793
794 Returns:
795 A string of length 'n', or less if EOF is reached.
796 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700797 # The data left available to read sits in [cur, end)
798 cur = self._stream.tell()
799 end = self._begin + self._chunksize
800 if n == -1 or cur + n > end:
801 n = end - cur
802 return self._stream.read(n)
John Asmuth864311d2014-04-24 15:46:08 -0400803
804
805class HttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700806 """Encapsulates a single HTTP request."""
John Asmuth864311d2014-04-24 15:46:08 -0400807
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700808 @util.positional(4)
809 def __init__(
810 self,
811 http,
812 postproc,
813 uri,
814 method="GET",
815 body=None,
816 headers=None,
817 methodId=None,
818 resumable=None,
819 ):
820 """Constructor for an HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -0400821
822 Args:
823 http: httplib2.Http, the transport object to use to make a request
824 postproc: callable, called on the HTTP response and content to transform
825 it into a data object before returning, or raising an exception
826 on an error.
827 uri: string, the absolute URI to send the request to
828 method: string, the HTTP method to use
829 body: string, the request body of the HTTP request,
830 headers: dict, the HTTP request headers
831 methodId: string, a unique identifier for the API method being called.
832 resumable: MediaUpload, None if this is not a resumbale request.
833 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700834 self.uri = uri
835 self.method = method
836 self.body = body
837 self.headers = headers or {}
838 self.methodId = methodId
839 self.http = http
840 self.postproc = postproc
841 self.resumable = resumable
842 self.response_callbacks = []
843 self._in_error_state = False
John Asmuth864311d2014-04-24 15:46:08 -0400844
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700845 # The size of the non-media part of the request.
846 self.body_size = len(self.body or "")
John Asmuth864311d2014-04-24 15:46:08 -0400847
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700848 # The resumable URI to send chunks to.
849 self.resumable_uri = None
John Asmuth864311d2014-04-24 15:46:08 -0400850
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700851 # The bytes that have been uploaded.
852 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -0400853
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700854 # Stubs for testing.
855 self._rand = random.random
856 self._sleep = time.sleep
John Asmuth864311d2014-04-24 15:46:08 -0400857
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700858 @util.positional(1)
859 def execute(self, http=None, num_retries=0):
860 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -0400861
862 Args:
863 http: httplib2.Http, an http object to be used in place of the
864 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500865 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400866 exponential backoff. If all retries fail, the raised HttpError
867 represents the last request. If zero (default), we attempt the
868 request only once.
869
870 Returns:
871 A deserialized object model of the response body as determined
872 by the postproc.
873
874 Raises:
875 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000876 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400877 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700878 if http is None:
879 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400880
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700881 if self.resumable:
882 body = None
883 while body is None:
884 _, body = self.next_chunk(http=http, num_retries=num_retries)
885 return body
John Asmuth864311d2014-04-24 15:46:08 -0400886
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700887 # Non-resumable case.
John Asmuth864311d2014-04-24 15:46:08 -0400888
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700889 if "content-length" not in self.headers:
890 self.headers["content-length"] = str(self.body_size)
891 # If the request URI is too long then turn it into a POST request.
892 # Assume that a GET request never contains a request body.
893 if len(self.uri) > MAX_URI_LENGTH and self.method == "GET":
894 self.method = "POST"
895 self.headers["x-http-method-override"] = "GET"
896 self.headers["content-type"] = "application/x-www-form-urlencoded"
897 parsed = urlparse(self.uri)
898 self.uri = urlunparse(
899 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, None)
900 )
901 self.body = parsed.query
902 self.headers["content-length"] = str(len(self.body))
John Asmuth864311d2014-04-24 15:46:08 -0400903
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700904 # Handle retries for server-side errors.
905 resp, content = _retry_request(
906 http,
907 num_retries,
908 "request",
909 self._sleep,
910 self._rand,
911 str(self.uri),
912 method=str(self.method),
913 body=self.body,
914 headers=self.headers,
915 )
John Asmuth864311d2014-04-24 15:46:08 -0400916
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700917 for callback in self.response_callbacks:
918 callback(resp)
919 if resp.status >= 300:
920 raise HttpError(resp, content, uri=self.uri)
921 return self.postproc(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -0400922
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700923 @util.positional(2)
924 def add_response_callback(self, cb):
925 """add_response_headers_callback
John Asmuth864311d2014-04-24 15:46:08 -0400926
927 Args:
928 cb: Callback to be called on receiving the response headers, of signature:
929
930 def cb(resp):
931 # Where resp is an instance of httplib2.Response
932 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700933 self.response_callbacks.append(cb)
John Asmuth864311d2014-04-24 15:46:08 -0400934
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700935 @util.positional(1)
936 def next_chunk(self, http=None, num_retries=0):
937 """Execute the next step of a resumable upload.
John Asmuth864311d2014-04-24 15:46:08 -0400938
939 Can only be used if the method being executed supports media uploads and
940 the MediaUpload object passed in was flagged as using resumable upload.
941
942 Example:
943
944 media = MediaFileUpload('cow.png', mimetype='image/png',
945 chunksize=1000, resumable=True)
946 request = farm.animals().insert(
947 id='cow',
948 name='cow.png',
949 media_body=media)
950
951 response = None
952 while response is None:
953 status, response = request.next_chunk()
954 if status:
955 print "Upload %d%% complete." % int(status.progress() * 100)
956
957
958 Args:
959 http: httplib2.Http, an http object to be used in place of the
960 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500961 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400962 exponential backoff. If all retries fail, the raised HttpError
963 represents the last request. If zero (default), we attempt the
964 request only once.
965
966 Returns:
967 (status, body): (ResumableMediaStatus, object)
968 The body will be None until the resumable media is fully uploaded.
969
970 Raises:
971 googleapiclient.errors.HttpError if the response was not a 2xx.
Tim Gates43fc0cf2020-04-21 08:03:25 +1000972 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -0400973 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700974 if http is None:
975 http = self.http
John Asmuth864311d2014-04-24 15:46:08 -0400976
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700977 if self.resumable.size() is None:
978 size = "*"
979 else:
980 size = str(self.resumable.size())
John Asmuth864311d2014-04-24 15:46:08 -0400981
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700982 if self.resumable_uri is None:
983 start_headers = copy.copy(self.headers)
984 start_headers["X-Upload-Content-Type"] = self.resumable.mimetype()
985 if size != "*":
986 start_headers["X-Upload-Content-Length"] = size
987 start_headers["content-length"] = str(self.body_size)
John Asmuth864311d2014-04-24 15:46:08 -0400988
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700989 resp, content = _retry_request(
990 http,
991 num_retries,
992 "resumable URI request",
993 self._sleep,
994 self._rand,
995 self.uri,
996 method=self.method,
997 body=self.body,
998 headers=start_headers,
999 )
John Asmuth864311d2014-04-24 15:46:08 -04001000
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001001 if resp.status == 200 and "location" in resp:
1002 self.resumable_uri = resp["location"]
1003 else:
1004 raise ResumableUploadError(resp, content)
1005 elif self._in_error_state:
1006 # If we are in an error state then query the server for current state of
1007 # the upload by sending an empty PUT and reading the 'range' header in
1008 # the response.
1009 headers = {"Content-Range": "bytes */%s" % size, "content-length": "0"}
1010 resp, content = http.request(self.resumable_uri, "PUT", headers=headers)
1011 status, body = self._process_response(resp, content)
1012 if body:
1013 # The upload was complete.
1014 return (status, body)
John Asmuth864311d2014-04-24 15:46:08 -04001015
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001016 if self.resumable.has_stream():
1017 data = self.resumable.stream()
1018 if self.resumable.chunksize() == -1:
1019 data.seek(self.resumable_progress)
1020 chunk_end = self.resumable.size() - self.resumable_progress - 1
1021 else:
1022 # Doing chunking with a stream, so wrap a slice of the stream.
1023 data = _StreamSlice(
1024 data, self.resumable_progress, self.resumable.chunksize()
1025 )
1026 chunk_end = min(
1027 self.resumable_progress + self.resumable.chunksize() - 1,
1028 self.resumable.size() - 1,
1029 )
1030 else:
1031 data = self.resumable.getbytes(
1032 self.resumable_progress, self.resumable.chunksize()
1033 )
John Asmuth864311d2014-04-24 15:46:08 -04001034
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001035 # A short read implies that we are at EOF, so finish the upload.
1036 if len(data) < self.resumable.chunksize():
1037 size = str(self.resumable_progress + len(data))
John Asmuth864311d2014-04-24 15:46:08 -04001038
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001039 chunk_end = self.resumable_progress + len(data) - 1
John Asmuth864311d2014-04-24 15:46:08 -04001040
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001041 headers = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001042 # Must set the content-length header here because httplib can't
1043 # calculate the size when working with _StreamSlice.
1044 "Content-Length": str(chunk_end - self.resumable_progress + 1),
John Asmuth864311d2014-04-24 15:46:08 -04001045 }
1046
Bu Sun Kimaf6035f2020-10-20 16:36:04 -06001047 # An empty file results in chunk_end = -1 and size = 0
1048 # sending "bytes 0--1/0" results in an invalid request
1049 # Only add header "Content-Range" if chunk_end != -1
1050 if chunk_end != -1:
1051 headers["Content-Range"] = "bytes %d-%d/%s" % (self.resumable_progress, chunk_end, size)
1052
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001053 for retry_num in range(num_retries + 1):
1054 if retry_num > 0:
1055 self._sleep(self._rand() * 2 ** retry_num)
1056 LOGGER.warning(
1057 "Retry #%d for media upload: %s %s, following status: %d"
1058 % (retry_num, self.method, self.uri, resp.status)
1059 )
John Asmuth864311d2014-04-24 15:46:08 -04001060
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001061 try:
1062 resp, content = http.request(
1063 self.resumable_uri, method="PUT", body=data, headers=headers
1064 )
1065 except:
1066 self._in_error_state = True
1067 raise
1068 if not _should_retry_response(resp.status, content):
1069 break
John Asmuth864311d2014-04-24 15:46:08 -04001070
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001071 return self._process_response(resp, content)
John Asmuth864311d2014-04-24 15:46:08 -04001072
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001073 def _process_response(self, resp, content):
1074 """Process the response from a single chunk upload.
John Asmuth864311d2014-04-24 15:46:08 -04001075
1076 Args:
1077 resp: httplib2.Response, the response object.
1078 content: string, the content of the response.
1079
1080 Returns:
1081 (status, body): (ResumableMediaStatus, object)
1082 The body will be None until the resumable media is fully uploaded.
1083
1084 Raises:
1085 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1086 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001087 if resp.status in [200, 201]:
1088 self._in_error_state = False
1089 return None, self.postproc(resp, content)
1090 elif resp.status == 308:
1091 self._in_error_state = False
1092 # A "308 Resume Incomplete" indicates we are not done.
1093 try:
1094 self.resumable_progress = int(resp["range"].split("-")[1]) + 1
1095 except KeyError:
1096 # If resp doesn't contain range header, resumable progress is 0
1097 self.resumable_progress = 0
1098 if "location" in resp:
1099 self.resumable_uri = resp["location"]
1100 else:
1101 self._in_error_state = True
1102 raise HttpError(resp, content, uri=self.uri)
John Asmuth864311d2014-04-24 15:46:08 -04001103
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001104 return (
1105 MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1106 None,
1107 )
John Asmuth864311d2014-04-24 15:46:08 -04001108
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001109 def to_json(self):
1110 """Returns a JSON representation of the HttpRequest."""
1111 d = copy.copy(self.__dict__)
1112 if d["resumable"] is not None:
1113 d["resumable"] = self.resumable.to_json()
1114 del d["http"]
1115 del d["postproc"]
1116 del d["_sleep"]
1117 del d["_rand"]
John Asmuth864311d2014-04-24 15:46:08 -04001118
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001119 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001120
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001121 @staticmethod
1122 def from_json(s, http, postproc):
1123 """Returns an HttpRequest populated with info from a JSON object."""
1124 d = json.loads(s)
1125 if d["resumable"] is not None:
1126 d["resumable"] = MediaUpload.new_from_json(d["resumable"])
1127 return HttpRequest(
1128 http,
1129 postproc,
1130 uri=d["uri"],
1131 method=d["method"],
1132 body=d["body"],
1133 headers=d["headers"],
1134 methodId=d["methodId"],
1135 resumable=d["resumable"],
1136 )
John Asmuth864311d2014-04-24 15:46:08 -04001137
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001138 @staticmethod
1139 def null_postproc(resp, contents):
1140 return resp, contents
1141
John Asmuth864311d2014-04-24 15:46:08 -04001142
1143class BatchHttpRequest(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001144 """Batches multiple HttpRequest objects into a single HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001145
1146 Example:
1147 from googleapiclient.http import BatchHttpRequest
1148
1149 def list_animals(request_id, response, exception):
1150 \"\"\"Do something with the animals list response.\"\"\"
1151 if exception is not None:
1152 # Do something with the exception.
1153 pass
1154 else:
1155 # Do something with the response.
1156 pass
1157
1158 def list_farmers(request_id, response, exception):
1159 \"\"\"Do something with the farmers list response.\"\"\"
1160 if exception is not None:
1161 # Do something with the exception.
1162 pass
1163 else:
1164 # Do something with the response.
1165 pass
1166
1167 service = build('farm', 'v2')
1168
1169 batch = BatchHttpRequest()
1170
1171 batch.add(service.animals().list(), list_animals)
1172 batch.add(service.farmers().list(), list_farmers)
1173 batch.execute(http=http)
1174 """
1175
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001176 @util.positional(1)
1177 def __init__(self, callback=None, batch_uri=None):
1178 """Constructor for a BatchHttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001179
1180 Args:
1181 callback: callable, A callback to be called for each response, of the
1182 form callback(id, response, exception). The first parameter is the
1183 request id, and the second is the deserialized response object. The
1184 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1185 occurred while processing the request, or None if no error occurred.
1186 batch_uri: string, URI to send batch requests to.
1187 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001188 if batch_uri is None:
1189 batch_uri = _LEGACY_BATCH_URI
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001190
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001191 if batch_uri == _LEGACY_BATCH_URI:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001192 LOGGER.warning(
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001193 "You have constructed a BatchHttpRequest using the legacy batch "
Brad Vogel6ddadd72020-05-15 10:02:04 -07001194 "endpoint %s. This endpoint will be turned down on August 12, 2020. "
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001195 "Please provide the API-specific endpoint or use "
1196 "service.new_batch_http_request(). For more details see "
1197 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1198 "and https://developers.google.com/api-client-library/python/guide/batch.",
1199 _LEGACY_BATCH_URI,
1200 )
1201 self._batch_uri = batch_uri
John Asmuth864311d2014-04-24 15:46:08 -04001202
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001203 # Global callback to be called for each individual response in the batch.
1204 self._callback = callback
John Asmuth864311d2014-04-24 15:46:08 -04001205
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001206 # A map from id to request.
1207 self._requests = {}
John Asmuth864311d2014-04-24 15:46:08 -04001208
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001209 # A map from id to callback.
1210 self._callbacks = {}
John Asmuth864311d2014-04-24 15:46:08 -04001211
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001212 # List of request ids, in the order in which they were added.
1213 self._order = []
John Asmuth864311d2014-04-24 15:46:08 -04001214
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001215 # The last auto generated id.
1216 self._last_auto_id = 0
John Asmuth864311d2014-04-24 15:46:08 -04001217
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001218 # Unique ID on which to base the Content-ID headers.
1219 self._base_id = None
John Asmuth864311d2014-04-24 15:46:08 -04001220
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001221 # A map from request id to (httplib2.Response, content) response pairs
1222 self._responses = {}
John Asmuth864311d2014-04-24 15:46:08 -04001223
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001224 # A map of id(Credentials) that have been refreshed.
1225 self._refreshed_credentials = {}
John Asmuth864311d2014-04-24 15:46:08 -04001226
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001227 def _refresh_and_apply_credentials(self, request, http):
1228 """Refresh the credentials and apply to the request.
John Asmuth864311d2014-04-24 15:46:08 -04001229
1230 Args:
1231 request: HttpRequest, the request.
1232 http: httplib2.Http, the global http object for the batch.
1233 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001234 # For the credentials to refresh, but only once per refresh_token
1235 # If there is no http per the request then refresh the http passed in
1236 # via execute()
1237 creds = None
1238 request_credentials = False
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001239
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001240 if request.http is not None:
1241 creds = _auth.get_credentials_from_http(request.http)
1242 request_credentials = True
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001243
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001244 if creds is None and http is not None:
1245 creds = _auth.get_credentials_from_http(http)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001246
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001247 if creds is not None:
1248 if id(creds) not in self._refreshed_credentials:
1249 _auth.refresh_credentials(creds)
1250 self._refreshed_credentials[id(creds)] = 1
John Asmuth864311d2014-04-24 15:46:08 -04001251
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001252 # Only apply the credentials if we are using the http object passed in,
1253 # otherwise apply() will get called during _serialize_request().
1254 if request.http is None or not request_credentials:
1255 _auth.apply_credentials(creds, request.headers)
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001256
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001257 def _id_to_header(self, id_):
1258 """Convert an id to a Content-ID header value.
John Asmuth864311d2014-04-24 15:46:08 -04001259
1260 Args:
1261 id_: string, identifier of individual request.
1262
1263 Returns:
1264 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1265 the value because Content-ID headers are supposed to be universally
1266 unique.
1267 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001268 if self._base_id is None:
1269 self._base_id = uuid.uuid4()
John Asmuth864311d2014-04-24 15:46:08 -04001270
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001271 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1272 # line folding works properly on Python 3; see
Marie J.I48f503f2020-05-15 13:32:11 -04001273 # https://github.com/googleapis/google-api-python-client/issues/164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001274 return "<%s + %s>" % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001275
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001276 def _header_to_id(self, header):
1277 """Convert a Content-ID header value to an id.
John Asmuth864311d2014-04-24 15:46:08 -04001278
1279 Presumes the Content-ID header conforms to the format that _id_to_header()
1280 returns.
1281
1282 Args:
1283 header: string, Content-ID header value.
1284
1285 Returns:
1286 The extracted id value.
1287
1288 Raises:
1289 BatchError if the header is not in the expected format.
1290 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001291 if header[0] != "<" or header[-1] != ">":
1292 raise BatchError("Invalid value for Content-ID: %s" % header)
1293 if "+" not in header:
1294 raise BatchError("Invalid value for Content-ID: %s" % header)
1295 base, id_ = header[1:-1].split(" + ", 1)
John Asmuth864311d2014-04-24 15:46:08 -04001296
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001297 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001298
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001299 def _serialize_request(self, request):
1300 """Convert an HttpRequest object into a string.
John Asmuth864311d2014-04-24 15:46:08 -04001301
1302 Args:
1303 request: HttpRequest, the request to serialize.
1304
1305 Returns:
1306 The request as a string in application/http format.
1307 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001308 # Construct status line
1309 parsed = urlparse(request.uri)
1310 request_line = urlunparse(
1311 ("", "", parsed.path, parsed.params, parsed.query, "")
John Asmuth864311d2014-04-24 15:46:08 -04001312 )
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001313 status_line = request.method + " " + request_line + " HTTP/1.1\n"
1314 major, minor = request.headers.get("content-type", "application/json").split(
1315 "/"
1316 )
1317 msg = MIMENonMultipart(major, minor)
1318 headers = request.headers.copy()
John Asmuth864311d2014-04-24 15:46:08 -04001319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001320 if request.http is not None:
1321 credentials = _auth.get_credentials_from_http(request.http)
1322 if credentials is not None:
1323 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001324
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001325 # MIMENonMultipart adds its own Content-Type header.
1326 if "content-type" in headers:
1327 del headers["content-type"]
John Asmuth864311d2014-04-24 15:46:08 -04001328
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001329 for key, value in six.iteritems(headers):
1330 msg[key] = value
1331 msg["Host"] = parsed.netloc
1332 msg.set_unixfrom(None)
John Asmuth864311d2014-04-24 15:46:08 -04001333
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001334 if request.body is not None:
1335 msg.set_payload(request.body)
1336 msg["content-length"] = str(len(request.body))
John Asmuth864311d2014-04-24 15:46:08 -04001337
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001338 # Serialize the mime message.
1339 fp = StringIO()
1340 # maxheaderlen=0 means don't line wrap headers.
1341 g = Generator(fp, maxheaderlen=0)
1342 g.flatten(msg, unixfrom=False)
1343 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001344
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001345 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001346
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001347 def _deserialize_response(self, payload):
1348 """Convert string into httplib2 response and content.
John Asmuth864311d2014-04-24 15:46:08 -04001349
1350 Args:
1351 payload: string, headers and body as a string.
1352
1353 Returns:
1354 A pair (resp, content), such as would be returned from httplib2.request.
1355 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001356 # Strip off the status line
1357 status_line, payload = payload.split("\n", 1)
1358 protocol, status, reason = status_line.split(" ", 2)
John Asmuth864311d2014-04-24 15:46:08 -04001359
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001360 # Parse the rest of the response
1361 parser = FeedParser()
1362 parser.feed(payload)
1363 msg = parser.close()
1364 msg["status"] = status
John Asmuth864311d2014-04-24 15:46:08 -04001365
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001366 # Create httplib2.Response from the parsed headers.
1367 resp = httplib2.Response(msg)
1368 resp.reason = reason
1369 resp.version = int(protocol.split("/", 1)[1].replace(".", ""))
John Asmuth864311d2014-04-24 15:46:08 -04001370
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001371 content = payload.split("\r\n\r\n", 1)[1]
John Asmuth864311d2014-04-24 15:46:08 -04001372
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001373 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001374
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001375 def _new_id(self):
1376 """Create a new id.
John Asmuth864311d2014-04-24 15:46:08 -04001377
1378 Auto incrementing number that avoids conflicts with ids already used.
1379
1380 Returns:
1381 string, a new unique id.
1382 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001383 self._last_auto_id += 1
1384 while str(self._last_auto_id) in self._requests:
1385 self._last_auto_id += 1
1386 return str(self._last_auto_id)
John Asmuth864311d2014-04-24 15:46:08 -04001387
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001388 @util.positional(2)
1389 def add(self, request, callback=None, request_id=None):
1390 """Add a new request.
John Asmuth864311d2014-04-24 15:46:08 -04001391
1392 Every callback added will be paired with a unique id, the request_id. That
1393 unique id will be passed back to the callback when the response comes back
1394 from the server. The default behavior is to have the library generate it's
1395 own unique id. If the caller passes in a request_id then they must ensure
1396 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001397 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001398 request id, to avoid such an error.
1399
1400 Args:
1401 request: HttpRequest, Request to add to the batch.
1402 callback: callable, A callback to be called for this response, of the
1403 form callback(id, response, exception). The first parameter is the
1404 request id, and the second is the deserialized response object. The
1405 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1406 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001407 request_id: string, A unique id for the request. The id will be passed
1408 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001409
1410 Returns:
1411 None
1412
1413 Raises:
1414 BatchError if a media request is added to a batch.
1415 KeyError is the request_id is not unique.
1416 """
Xinan Line2dccec2018-12-07 05:28:33 +09001417
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001418 if len(self._order) >= MAX_BATCH_LIMIT:
1419 raise BatchError(
1420 "Exceeded the maximum calls(%d) in a single batch request."
1421 % MAX_BATCH_LIMIT
1422 )
1423 if request_id is None:
1424 request_id = self._new_id()
1425 if request.resumable is not None:
1426 raise BatchError("Media requests cannot be used in a batch request.")
1427 if request_id in self._requests:
1428 raise KeyError("A request with this ID already exists: %s" % request_id)
1429 self._requests[request_id] = request
1430 self._callbacks[request_id] = callback
1431 self._order.append(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001432
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001433 def _execute(self, http, order, requests):
1434 """Serialize batch request, send to server, process response.
John Asmuth864311d2014-04-24 15:46:08 -04001435
1436 Args:
1437 http: httplib2.Http, an http object to be used to make the request with.
1438 order: list, list of request ids in the order they were added to the
1439 batch.
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001440 requests: list, list of request objects to send.
John Asmuth864311d2014-04-24 15:46:08 -04001441
1442 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001443 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001444 googleapiclient.errors.BatchError if the response is the wrong format.
1445 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001446 message = MIMEMultipart("mixed")
1447 # Message should not write out it's own headers.
1448 setattr(message, "_write_headers", lambda self: None)
John Asmuth864311d2014-04-24 15:46:08 -04001449
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001450 # Add all the individual requests.
1451 for request_id in order:
1452 request = requests[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001453
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001454 msg = MIMENonMultipart("application", "http")
1455 msg["Content-Transfer-Encoding"] = "binary"
1456 msg["Content-ID"] = self._id_to_header(request_id)
John Asmuth864311d2014-04-24 15:46:08 -04001457
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001458 body = self._serialize_request(request)
1459 msg.set_payload(body)
1460 message.attach(msg)
John Asmuth864311d2014-04-24 15:46:08 -04001461
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001462 # encode the body: note that we can't use `as_string`, because
1463 # it plays games with `From ` lines.
1464 fp = StringIO()
1465 g = Generator(fp, mangle_from_=False)
1466 g.flatten(message, unixfrom=False)
1467 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001468
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001469 headers = {}
1470 headers["content-type"] = (
1471 "multipart/mixed; " 'boundary="%s"'
1472 ) % message.get_boundary()
John Asmuth864311d2014-04-24 15:46:08 -04001473
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001474 resp, content = http.request(
1475 self._batch_uri, method="POST", body=body, headers=headers
1476 )
John Asmuth864311d2014-04-24 15:46:08 -04001477
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001478 if resp.status >= 300:
1479 raise HttpError(resp, content, uri=self._batch_uri)
John Asmuth864311d2014-04-24 15:46:08 -04001480
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001481 # Prepend with a content-type header so FeedParser can handle it.
1482 header = "content-type: %s\r\n\r\n" % resp["content-type"]
1483 # PY3's FeedParser only accepts unicode. So we should decode content
1484 # here, and encode each payload again.
1485 if six.PY3:
1486 content = content.decode("utf-8")
1487 for_parser = header + content
John Asmuth864311d2014-04-24 15:46:08 -04001488
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001489 parser = FeedParser()
1490 parser.feed(for_parser)
1491 mime_response = parser.close()
John Asmuth864311d2014-04-24 15:46:08 -04001492
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001493 if not mime_response.is_multipart():
1494 raise BatchError(
1495 "Response not in multipart/mixed format.", resp=resp, content=content
1496 )
John Asmuth864311d2014-04-24 15:46:08 -04001497
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001498 for part in mime_response.get_payload():
1499 request_id = self._header_to_id(part["Content-ID"])
1500 response, content = self._deserialize_response(part.get_payload())
1501 # We encode content here to emulate normal http response.
1502 if isinstance(content, six.text_type):
1503 content = content.encode("utf-8")
1504 self._responses[request_id] = (response, content)
John Asmuth864311d2014-04-24 15:46:08 -04001505
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001506 @util.positional(1)
1507 def execute(self, http=None):
1508 """Execute all the requests as a single batched HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -04001509
1510 Args:
1511 http: httplib2.Http, an http object to be used in place of the one the
1512 HttpRequest request object was constructed with. If one isn't supplied
1513 then use a http object from the requests in this batch.
1514
1515 Returns:
1516 None
1517
1518 Raises:
Tim Gates43fc0cf2020-04-21 08:03:25 +10001519 httplib2.HttpLib2Error if a transport error has occurred.
John Asmuth864311d2014-04-24 15:46:08 -04001520 googleapiclient.errors.BatchError if the response is the wrong format.
1521 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001522 # If we have no requests return
1523 if len(self._order) == 0:
1524 return None
John Asmuth864311d2014-04-24 15:46:08 -04001525
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001526 # If http is not supplied use the first valid one given in the requests.
1527 if http is None:
1528 for request_id in self._order:
1529 request = self._requests[request_id]
1530 if request is not None:
1531 http = request.http
1532 break
John Asmuth864311d2014-04-24 15:46:08 -04001533
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001534 if http is None:
1535 raise ValueError("Missing a valid http object.")
John Asmuth864311d2014-04-24 15:46:08 -04001536
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001537 # Special case for OAuth2Credentials-style objects which have not yet been
1538 # refreshed with an initial access_token.
1539 creds = _auth.get_credentials_from_http(http)
1540 if creds is not None:
1541 if not _auth.is_valid(creds):
1542 LOGGER.info("Attempting refresh to obtain initial access_token")
1543 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001544
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001545 self._execute(http, self._order, self._requests)
John Asmuth864311d2014-04-24 15:46:08 -04001546
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001547 # Loop over all the requests and check for 401s. For each 401 request the
1548 # credentials should be refreshed and then sent again in a separate batch.
1549 redo_requests = {}
1550 redo_order = []
John Asmuth864311d2014-04-24 15:46:08 -04001551
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001552 for request_id in self._order:
1553 resp, content = self._responses[request_id]
1554 if resp["status"] == "401":
1555 redo_order.append(request_id)
1556 request = self._requests[request_id]
1557 self._refresh_and_apply_credentials(request, http)
1558 redo_requests[request_id] = request
John Asmuth864311d2014-04-24 15:46:08 -04001559
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001560 if redo_requests:
1561 self._execute(http, redo_order, redo_requests)
John Asmuth864311d2014-04-24 15:46:08 -04001562
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001563 # Now process all callbacks that are erroring, and raise an exception for
1564 # ones that return a non-2xx response? Or add extra parameter to callback
1565 # that contains an HttpError?
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]
John Asmuth864311d2014-04-24 15:46:08 -04001569
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001570 request = self._requests[request_id]
1571 callback = self._callbacks[request_id]
John Asmuth864311d2014-04-24 15:46:08 -04001572
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001573 response = None
1574 exception = None
1575 try:
1576 if resp.status >= 300:
1577 raise HttpError(resp, content, uri=request.uri)
1578 response = request.postproc(resp, content)
1579 except HttpError as e:
1580 exception = e
John Asmuth864311d2014-04-24 15:46:08 -04001581
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001582 if callback is not None:
1583 callback(request_id, response, exception)
1584 if self._callback is not None:
1585 self._callback(request_id, response, exception)
John Asmuth864311d2014-04-24 15:46:08 -04001586
1587
1588class HttpRequestMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001589 """Mock of HttpRequest.
John Asmuth864311d2014-04-24 15:46:08 -04001590
1591 Do not construct directly, instead use RequestMockBuilder.
1592 """
1593
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001594 def __init__(self, resp, content, postproc):
1595 """Constructor for HttpRequestMock
John Asmuth864311d2014-04-24 15:46:08 -04001596
1597 Args:
1598 resp: httplib2.Response, the response to emulate coming from the request
1599 content: string, the response body
1600 postproc: callable, the post processing function usually supplied by
1601 the model class. See model.JsonModel.response() as an example.
1602 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001603 self.resp = resp
1604 self.content = content
1605 self.postproc = postproc
1606 if resp is None:
1607 self.resp = httplib2.Response({"status": 200, "reason": "OK"})
1608 if "reason" in self.resp:
1609 self.resp.reason = self.resp["reason"]
John Asmuth864311d2014-04-24 15:46:08 -04001610
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001611 def execute(self, http=None):
1612 """Execute the request.
John Asmuth864311d2014-04-24 15:46:08 -04001613
1614 Same behavior as HttpRequest.execute(), but the response is
1615 mocked and not really from an HTTP request/response.
1616 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001617 return self.postproc(self.resp, self.content)
John Asmuth864311d2014-04-24 15:46:08 -04001618
1619
1620class RequestMockBuilder(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001621 """A simple mock of HttpRequest
John Asmuth864311d2014-04-24 15:46:08 -04001622
1623 Pass in a dictionary to the constructor that maps request methodIds to
1624 tuples of (httplib2.Response, content, opt_expected_body) that should be
1625 returned when that method is called. None may also be passed in for the
1626 httplib2.Response, in which case a 200 OK response will be generated.
1627 If an opt_expected_body (str or dict) is provided, it will be compared to
1628 the body and UnexpectedBodyError will be raised on inequality.
1629
1630 Example:
1631 response = '{"data": {"id": "tag:google.c...'
1632 requestBuilder = RequestMockBuilder(
1633 {
1634 'plus.activities.get': (None, response),
1635 }
1636 )
1637 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1638
1639 Methods that you do not supply a response for will return a
1640 200 OK with an empty string as the response content or raise an excpetion
1641 if check_unexpected is set to True. The methodId is taken from the rpcName
1642 in the discovery document.
1643
1644 For more details see the project wiki.
1645 """
1646
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001647 def __init__(self, responses, check_unexpected=False):
1648 """Constructor for RequestMockBuilder
John Asmuth864311d2014-04-24 15:46:08 -04001649
1650 The constructed object should be a callable object
1651 that can replace the class HttpResponse.
1652
1653 responses - A dictionary that maps methodIds into tuples
1654 of (httplib2.Response, content). The methodId
1655 comes from the 'rpcName' field in the discovery
1656 document.
1657 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1658 should be raised on unsupplied method.
1659 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001660 self.responses = responses
1661 self.check_unexpected = check_unexpected
John Asmuth864311d2014-04-24 15:46:08 -04001662
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001663 def __call__(
1664 self,
1665 http,
1666 postproc,
1667 uri,
1668 method="GET",
1669 body=None,
1670 headers=None,
1671 methodId=None,
1672 resumable=None,
1673 ):
1674 """Implements the callable interface that discovery.build() expects
John Asmuth864311d2014-04-24 15:46:08 -04001675 of requestBuilder, which is to build an object compatible with
1676 HttpRequest.execute(). See that method for the description of the
1677 parameters and the expected response.
1678 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001679 if methodId in self.responses:
1680 response = self.responses[methodId]
1681 resp, content = response[:2]
1682 if len(response) > 2:
1683 # Test the body against the supplied expected_body.
1684 expected_body = response[2]
1685 if bool(expected_body) != bool(body):
1686 # Not expecting a body and provided one
1687 # or expecting a body and not provided one.
1688 raise UnexpectedBodyError(expected_body, body)
1689 if isinstance(expected_body, str):
1690 expected_body = json.loads(expected_body)
1691 body = json.loads(body)
1692 if body != expected_body:
1693 raise UnexpectedBodyError(expected_body, body)
1694 return HttpRequestMock(resp, content, postproc)
1695 elif self.check_unexpected:
1696 raise UnexpectedMethodError(methodId=methodId)
1697 else:
1698 model = JsonModel(False)
1699 return HttpRequestMock(None, "{}", model.response)
John Asmuth864311d2014-04-24 15:46:08 -04001700
1701
1702class HttpMock(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001703 """Mock of httplib2.Http"""
John Asmuth864311d2014-04-24 15:46:08 -04001704
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001705 def __init__(self, filename=None, headers=None):
1706 """
John Asmuth864311d2014-04-24 15:46:08 -04001707 Args:
1708 filename: string, absolute filename to read response from
1709 headers: dict, header to return with response
1710 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001711 if headers is None:
1712 headers = {"status": "200"}
1713 if filename:
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001714 with open(filename, "rb") as f:
1715 self.data = f.read()
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001716 else:
1717 self.data = None
1718 self.response_headers = headers
1719 self.headers = None
1720 self.uri = None
1721 self.method = None
1722 self.body = None
1723 self.headers = None
John Asmuth864311d2014-04-24 15:46:08 -04001724
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001725 def request(
1726 self,
1727 uri,
1728 method="GET",
1729 body=None,
1730 headers=None,
1731 redirections=1,
1732 connection_type=None,
1733 ):
1734 self.uri = uri
1735 self.method = method
1736 self.body = body
1737 self.headers = headers
1738 return httplib2.Response(self.response_headers), self.data
John Asmuth864311d2014-04-24 15:46:08 -04001739
Bu Sun Kim98888da2020-09-23 11:10:39 -06001740 def close(self):
1741 return None
John Asmuth864311d2014-04-24 15:46:08 -04001742
1743class HttpMockSequence(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001744 """Mock of httplib2.Http
John Asmuth864311d2014-04-24 15:46:08 -04001745
1746 Mocks a sequence of calls to request returning different responses for each
1747 call. Create an instance initialized with the desired response headers
1748 and content and then use as if an httplib2.Http instance.
1749
1750 http = HttpMockSequence([
1751 ({'status': '401'}, ''),
1752 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1753 ({'status': '200'}, 'echo_request_headers'),
1754 ])
1755 resp, content = http.request("http://examples.com")
1756
1757 There are special values you can pass in for content to trigger
1758 behavours that are helpful in testing.
1759
1760 'echo_request_headers' means return the request headers in the response body
1761 'echo_request_headers_as_json' means return the request headers in
1762 the response body
1763 'echo_request_body' means return the request body in the response body
1764 'echo_request_uri' means return the request uri in the response body
1765 """
1766
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001767 def __init__(self, iterable):
1768 """
John Asmuth864311d2014-04-24 15:46:08 -04001769 Args:
1770 iterable: iterable, a sequence of pairs of (headers, body)
1771 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001772 self._iterable = iterable
1773 self.follow_redirects = True
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001774 self.request_sequence = list()
John Asmuth864311d2014-04-24 15:46:08 -04001775
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001776 def request(
1777 self,
1778 uri,
1779 method="GET",
1780 body=None,
1781 headers=None,
1782 redirections=1,
1783 connection_type=None,
1784 ):
Dmitry Frenkelf3348f92020-07-15 13:05:58 -07001785 # Remember the request so after the fact this mock can be examined
1786 self.request_sequence.append((uri, method, body, headers))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001787 resp, content = self._iterable.pop(0)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001788 content = six.ensure_binary(content)
1789
1790 if content == b"echo_request_headers":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001791 content = headers
Matt McDonaldef6420a2020-04-14 16:28:13 -04001792 elif content == b"echo_request_headers_as_json":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001793 content = json.dumps(headers)
Matt McDonaldef6420a2020-04-14 16:28:13 -04001794 elif content == b"echo_request_body":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001795 if hasattr(body, "read"):
1796 content = body.read()
1797 else:
1798 content = body
Matt McDonaldef6420a2020-04-14 16:28:13 -04001799 elif content == b"echo_request_uri":
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001800 content = uri
1801 if isinstance(content, six.text_type):
1802 content = content.encode("utf-8")
1803 return httplib2.Response(resp), content
John Asmuth864311d2014-04-24 15:46:08 -04001804
1805
1806def set_user_agent(http, user_agent):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001807 """Set the user-agent on every request.
John Asmuth864311d2014-04-24 15:46:08 -04001808
1809 Args:
1810 http - An instance of httplib2.Http
1811 or something that acts like it.
1812 user_agent: string, the value for the user-agent header.
1813
1814 Returns:
1815 A modified instance of http that was passed in.
1816
1817 Example:
1818
1819 h = httplib2.Http()
1820 h = set_user_agent(h, "my-app-name/6.0")
1821
1822 Most of the time the user-agent will be set doing auth, this is for the rare
1823 cases where you are accessing an unauthenticated endpoint.
1824 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001825 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001826
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001827 # The closure that will replace 'httplib2.Http.request'.
1828 def new_request(
1829 uri,
1830 method="GET",
1831 body=None,
1832 headers=None,
1833 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1834 connection_type=None,
1835 ):
1836 """Modify the request headers to add the user-agent."""
1837 if headers is None:
1838 headers = {}
1839 if "user-agent" in headers:
1840 headers["user-agent"] = user_agent + " " + headers["user-agent"]
1841 else:
1842 headers["user-agent"] = user_agent
1843 resp, content = request_orig(
1844 uri,
1845 method=method,
1846 body=body,
1847 headers=headers,
1848 redirections=redirections,
1849 connection_type=connection_type,
1850 )
1851 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001852
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001853 http.request = new_request
1854 return http
John Asmuth864311d2014-04-24 15:46:08 -04001855
1856
1857def tunnel_patch(http):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001858 """Tunnel PATCH requests over POST.
John Asmuth864311d2014-04-24 15:46:08 -04001859 Args:
1860 http - An instance of httplib2.Http
1861 or something that acts like it.
1862
1863 Returns:
1864 A modified instance of http that was passed in.
1865
1866 Example:
1867
1868 h = httplib2.Http()
1869 h = tunnel_patch(h, "my-app-name/6.0")
1870
1871 Useful if you are running on a platform that doesn't support PATCH.
1872 Apply this last if you are using OAuth 1.0, as changing the method
1873 will result in a different signature.
1874 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001875 request_orig = http.request
John Asmuth864311d2014-04-24 15:46:08 -04001876
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001877 # The closure that will replace 'httplib2.Http.request'.
1878 def new_request(
1879 uri,
1880 method="GET",
1881 body=None,
1882 headers=None,
1883 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1884 connection_type=None,
1885 ):
1886 """Modify the request headers to add the user-agent."""
1887 if headers is None:
1888 headers = {}
1889 if method == "PATCH":
1890 if "oauth_token" in headers.get("authorization", ""):
1891 LOGGER.warning(
1892 "OAuth 1.0 request made with Credentials after tunnel_patch."
1893 )
1894 headers["x-http-method-override"] = "PATCH"
1895 method = "POST"
1896 resp, content = request_orig(
1897 uri,
1898 method=method,
1899 body=body,
1900 headers=headers,
1901 redirections=redirections,
1902 connection_type=connection_type,
1903 )
1904 return resp, content
John Asmuth864311d2014-04-24 15:46:08 -04001905
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001906 http.request = new_request
1907 return http
Igor Maravić22435292017-01-19 22:28:22 +01001908
1909
1910def build_http():
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001911 """Builds httplib2.Http object
Igor Maravić22435292017-01-19 22:28:22 +01001912
1913 Returns:
1914 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1915 To override default timeout call
1916
1917 socket.setdefaulttimeout(timeout_in_sec)
1918
1919 before interacting with this method.
1920 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001921 if socket.getdefaulttimeout() is not None:
1922 http_timeout = socket.getdefaulttimeout()
1923 else:
1924 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001925 http = httplib2.Http(timeout=http_timeout)
1926 # 308's are used by several Google APIs (Drive, YouTube)
1927 # for Resumable Uploads rather than Permanent Redirects.
1928 # This asks httplib2 to exclude 308s from the status codes
1929 # it treats as redirects
Bu Sun Kima480d532020-03-13 12:52:22 -07001930 try:
1931 http.redirect_codes = http.redirect_codes - {308}
1932 except AttributeError:
1933 # Apache Beam tests depend on this library and cannot
1934 # currently upgrade their httplib2 version
1935 # http.redirect_codes does not exist in previous versions
1936 # of httplib2, so pass
1937 pass
Bu Sun Kimb3b773f2020-03-11 12:58:16 -07001938
1939 return http