blob: 4949d0cf35600a7ea1a46a9e0904600b725740bc [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Classes to encapsulate a single HTTP request.
16
17The classes implement a command pattern, with every
18object supporting an execute() method that does the
cspeidelfbaf9d72018-05-10 12:50:12 -060019actual HTTP request.
John Asmuth864311d2014-04-24 15:46:08 -040020"""
INADA Naoki0bceb332014-08-20 15:27:52 +090021from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090022import six
eesheeshc6425a02016-02-12 15:07:06 +000023from six.moves import http_client
INADA Naokie4ea1a92015-03-04 03:45:42 +090024from six.moves import range
John Asmuth864311d2014-04-24 15:46:08 -040025
26__author__ = 'jcgregorio@google.com (Joe Gregorio)'
27
Pat Ferateed9affd2015-03-03 16:03:15 -080028from six import BytesIO, StringIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080029from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
Pat Ferateed9affd2015-03-03 16:03:15 -080030
John Asmuth864311d2014-04-24 15:46:08 -040031import base64
32import copy
33import gzip
34import httplib2
Craig Citro6ae34d72014-08-18 23:10:09 -070035import json
John Asmuth864311d2014-04-24 15:46:08 -040036import logging
John Asmuth864311d2014-04-24 15:46:08 -040037import mimetypes
38import os
39import random
eesheeshc6425a02016-02-12 15:07:06 +000040import socket
John Asmuth864311d2014-04-24 15:46:08 -040041import sys
42import time
John Asmuth864311d2014-04-24 15:46:08 -040043import uuid
44
Tay Ray Chuan3146c922016-04-20 16:38:19 +000045# TODO(issue 221): Remove this conditional import jibbajabba.
46try:
47 import ssl
48except ImportError:
49 _ssl_SSLError = object()
50else:
51 _ssl_SSLError = ssl.SSLError
52
John Asmuth864311d2014-04-24 15:46:08 -040053from email.generator import Generator
54from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
56from email.parser import FeedParser
Pat Ferateb240c172015-03-03 16:23:51 -080057
Helen Koikede13e3b2018-04-26 16:05:16 -030058from googleapiclient import _helpers as util
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070059
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070060from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080061from googleapiclient.errors import BatchError
62from googleapiclient.errors import HttpError
63from googleapiclient.errors import InvalidChunkSizeError
64from googleapiclient.errors import ResumableUploadError
65from googleapiclient.errors import UnexpectedBodyError
66from googleapiclient.errors import UnexpectedMethodError
67from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040068
69
Emmett Butler09699152016-02-08 14:26:00 -080070LOGGER = logging.getLogger(__name__)
71
Craig Citrod1aa6912018-02-28 15:31:08 -080072DEFAULT_CHUNK_SIZE = 100*1024*1024
John Asmuth864311d2014-04-24 15:46:08 -040073
74MAX_URI_LENGTH = 2048
75
Xinan Line2dccec2018-12-07 05:28:33 +090076MAX_BATCH_LIMIT = 1000
77
eesheeshc6425a02016-02-12 15:07:06 +000078_TOO_MANY_REQUESTS = 429
79
Igor Maravić22435292017-01-19 22:28:22 +010080DEFAULT_HTTP_TIMEOUT_SEC = 60
81
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070082_LEGACY_BATCH_URI = 'https://www.googleapis.com/batch'
83
eesheeshc6425a02016-02-12 15:07:06 +000084
85def _should_retry_response(resp_status, content):
86 """Determines whether a response should be retried.
87
88 Args:
89 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080090 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000091
92 Returns:
93 True if the response should be retried, otherwise False.
94 """
95 # Retry on 5xx errors.
96 if resp_status >= 500:
97 return True
98
99 # Retry on 429 errors.
100 if resp_status == _TOO_MANY_REQUESTS:
101 return True
102
103 # For 403 errors, we have to check for the `reason` in the response to
104 # determine if we should retry.
105 if resp_status == six.moves.http_client.FORBIDDEN:
106 # If there's no details about the 403 type, don't retry.
107 if not content:
108 return False
109
110 # Content is in JSON format.
111 try:
112 data = json.loads(content.decode('utf-8'))
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -0800113 if isinstance(data, dict):
114 reason = data['error']['errors'][0]['reason']
115 else:
116 reason = data[0]['error']['errors']['reason']
eesheeshc6425a02016-02-12 15:07:06 +0000117 except (UnicodeDecodeError, ValueError, KeyError):
118 LOGGER.warning('Invalid JSON content from response: %s', content)
119 return False
120
121 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
122
123 # Only retry on rate limit related failures.
124 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
125 return True
126
127 # Everything else is a success or non-retriable so break.
128 return False
129
John Asmuth864311d2014-04-24 15:46:08 -0400130
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100131def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
132 **kwargs):
133 """Retries an HTTP request multiple times while handling errors.
134
135 If after all retries the request still fails, last error is either returned as
136 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
137
138 Args:
139 http: Http object to be used to execute request.
140 num_retries: Maximum number of retries.
141 req_type: Type of the request (used for logging retries).
142 sleep, rand: Functions to sleep for random time between retries.
143 uri: URI to be requested.
144 method: HTTP method to be used.
145 args, kwargs: Additional arguments passed to http.request.
146
147 Returns:
148 resp, content - Response from the http request (may be HTTP 5xx).
149 """
150 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000151 content = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100152 for retry_num in range(num_retries + 1):
153 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000154 # Sleep before retrying.
155 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800156 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000157 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
158 sleep_time, retry_num, num_retries, req_type, method, uri,
159 resp.status if resp else exception)
160 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100161
162 try:
eesheeshc6425a02016-02-12 15:07:06 +0000163 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100164 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000165 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000166 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000167 exception = ssl_error
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700168 except socket.timeout as socket_timeout:
169 # It's important that this be before socket.error as it's a subclass
170 # socket.timeout has no errorcode
171 exception = socket_timeout
eesheeshc6425a02016-02-12 15:07:06 +0000172 except socket.error as socket_error:
173 # errno's contents differ by platform, so we have to match by name.
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700174 if socket.errno.errorcode.get(socket_error.errno) not in {
175 'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED'}:
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100176 raise
eesheeshc6425a02016-02-12 15:07:06 +0000177 exception = socket_error
Bashir Sadjadc35150f2018-06-25 11:46:09 -0400178 except httplib2.ServerNotFoundError as server_not_found_error:
179 exception = server_not_found_error
eesheeshc6425a02016-02-12 15:07:06 +0000180
181 if exception:
182 if retry_num == num_retries:
183 raise exception
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100184 else:
185 continue
eesheeshc6425a02016-02-12 15:07:06 +0000186
187 if not _should_retry_response(resp.status, content):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100188 break
189
190 return resp, content
191
192
John Asmuth864311d2014-04-24 15:46:08 -0400193class MediaUploadProgress(object):
194 """Status of a resumable upload."""
195
196 def __init__(self, resumable_progress, total_size):
197 """Constructor.
198
199 Args:
200 resumable_progress: int, bytes sent so far.
201 total_size: int, total bytes in complete upload, or None if the total
202 upload size isn't known ahead of time.
203 """
204 self.resumable_progress = resumable_progress
205 self.total_size = total_size
206
207 def progress(self):
208 """Percent of upload completed, as a float.
209
210 Returns:
211 the percentage complete as a float, returning 0.0 if the total size of
212 the upload is unknown.
213 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300214 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400215 return float(self.resumable_progress) / float(self.total_size)
216 else:
217 return 0.0
218
219
220class MediaDownloadProgress(object):
221 """Status of a resumable download."""
222
223 def __init__(self, resumable_progress, total_size):
224 """Constructor.
225
226 Args:
227 resumable_progress: int, bytes received so far.
228 total_size: int, total bytes in complete download.
229 """
230 self.resumable_progress = resumable_progress
231 self.total_size = total_size
232
233 def progress(self):
234 """Percent of download completed, as a float.
235
236 Returns:
237 the percentage complete as a float, returning 0.0 if the total size of
238 the download is unknown.
239 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300240 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400241 return float(self.resumable_progress) / float(self.total_size)
242 else:
243 return 0.0
244
245
246class MediaUpload(object):
247 """Describes a media object to upload.
248
249 Base class that defines the interface of MediaUpload subclasses.
250
251 Note that subclasses of MediaUpload may allow you to control the chunksize
252 when uploading a media object. It is important to keep the size of the chunk
253 as large as possible to keep the upload efficient. Other factors may influence
254 the size of the chunk you use, particularly if you are working in an
255 environment where individual HTTP requests may have a hardcoded time limit,
256 such as under certain classes of requests under Google App Engine.
257
258 Streams are io.Base compatible objects that support seek(). Some MediaUpload
259 subclasses support using streams directly to upload data. Support for
260 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
261 platform that stream will be used for uploading the media object. The support
262 for streaming is indicated by has_stream() returning True. The stream() method
263 should return an io.Base object that supports seek(). On platforms where the
264 underlying httplib module supports streaming, for example Python 2.6 and
265 later, the stream will be passed into the http library which will result in
266 less memory being used and possibly faster uploads.
267
268 If you need to upload media that can't be uploaded using any of the existing
269 MediaUpload sub-class then you can sub-class MediaUpload for your particular
270 needs.
271 """
272
273 def chunksize(self):
274 """Chunk size for resumable uploads.
275
276 Returns:
277 Chunk size in bytes.
278 """
279 raise NotImplementedError()
280
281 def mimetype(self):
282 """Mime type of the body.
283
284 Returns:
285 Mime type.
286 """
287 return 'application/octet-stream'
288
289 def size(self):
290 """Size of upload.
291
292 Returns:
293 Size of the body, or None of the size is unknown.
294 """
295 return None
296
297 def resumable(self):
298 """Whether this upload is resumable.
299
300 Returns:
301 True if resumable upload or False.
302 """
303 return False
304
305 def getbytes(self, begin, end):
306 """Get bytes from the media.
307
308 Args:
309 begin: int, offset from beginning of file.
310 length: int, number of bytes to read, starting at begin.
311
312 Returns:
313 A string of bytes read. May be shorter than length if EOF was reached
314 first.
315 """
316 raise NotImplementedError()
317
318 def has_stream(self):
319 """Does the underlying upload support a streaming interface.
320
321 Streaming means it is an io.IOBase subclass that supports seek, i.e.
322 seekable() returns True.
323
324 Returns:
325 True if the call to stream() will return an instance of a seekable io.Base
326 subclass.
327 """
328 return False
329
330 def stream(self):
331 """A stream interface to the data being uploaded.
332
333 Returns:
334 The returned value is an io.IOBase subclass that supports seek, i.e.
335 seekable() returns True.
336 """
337 raise NotImplementedError()
338
339 @util.positional(1)
340 def _to_json(self, strip=None):
341 """Utility function for creating a JSON representation of a MediaUpload.
342
343 Args:
344 strip: array, An array of names of members to not include in the JSON.
345
346 Returns:
347 string, a JSON representation of this instance, suitable to pass to
348 from_json().
349 """
350 t = type(self)
351 d = copy.copy(self.__dict__)
352 if strip is not None:
353 for member in strip:
354 del d[member]
355 d['_class'] = t.__name__
356 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700357 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400358
359 def to_json(self):
360 """Create a JSON representation of an instance of MediaUpload.
361
362 Returns:
363 string, a JSON representation of this instance, suitable to pass to
364 from_json().
365 """
366 return self._to_json()
367
368 @classmethod
369 def new_from_json(cls, s):
370 """Utility class method to instantiate a MediaUpload subclass from a JSON
371 representation produced by to_json().
372
373 Args:
374 s: string, JSON from to_json().
375
376 Returns:
377 An instance of the subclass of MediaUpload that was serialized with
378 to_json().
379 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700380 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400381 # Find and call the right classmethod from_json() to restore the object.
382 module = data['_module']
383 m = __import__(module, fromlist=module.split('.')[:-1])
384 kls = getattr(m, data['_class'])
385 from_json = getattr(kls, 'from_json')
386 return from_json(s)
387
388
389class MediaIoBaseUpload(MediaUpload):
390 """A MediaUpload for a io.Base objects.
391
392 Note that the Python file object is compatible with io.Base and can be used
393 with this class also.
394
Pat Ferateed9affd2015-03-03 16:03:15 -0800395 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400396 media = MediaIoBaseUpload(fh, mimetype='image/png',
397 chunksize=1024*1024, resumable=True)
398 farm.animals().insert(
399 id='cow',
400 name='cow.png',
401 media_body=media).execute()
402
403 Depending on the platform you are working on, you may pass -1 as the
404 chunksize, which indicates that the entire file should be uploaded in a single
405 request. If the underlying platform supports streams, such as Python 2.6 or
406 later, then this can be very efficient as it avoids multiple connections, and
407 also avoids loading the entire file into memory before sending it. Note that
408 Google App Engine has a 5MB limit on request size, so you should never set
409 your chunksize larger than 5MB, or to -1.
410 """
411
412 @util.positional(3)
413 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
414 resumable=False):
415 """Constructor.
416
417 Args:
418 fd: io.Base or file object, The source of the bytes to upload. MUST be
419 opened in blocking mode, do not use streams opened in non-blocking mode.
420 The given stream must be seekable, that is, it must be able to call
421 seek() on fd.
422 mimetype: string, Mime-type of the file.
423 chunksize: int, File will be uploaded in chunks of this many bytes. Only
424 used if resumable=True. Pass in a value of -1 if the file is to be
425 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
426 on request size, so you should never set your chunksize larger than 5MB,
427 or to -1.
428 resumable: bool, True if this is a resumable upload. False means upload
429 in a single request.
430 """
431 super(MediaIoBaseUpload, self).__init__()
432 self._fd = fd
433 self._mimetype = mimetype
434 if not (chunksize == -1 or chunksize > 0):
435 raise InvalidChunkSizeError()
436 self._chunksize = chunksize
437 self._resumable = resumable
438
439 self._fd.seek(0, os.SEEK_END)
440 self._size = self._fd.tell()
441
442 def chunksize(self):
443 """Chunk size for resumable uploads.
444
445 Returns:
446 Chunk size in bytes.
447 """
448 return self._chunksize
449
450 def mimetype(self):
451 """Mime type of the body.
452
453 Returns:
454 Mime type.
455 """
456 return self._mimetype
457
458 def size(self):
459 """Size of upload.
460
461 Returns:
462 Size of the body, or None of the size is unknown.
463 """
464 return self._size
465
466 def resumable(self):
467 """Whether this upload is resumable.
468
469 Returns:
470 True if resumable upload or False.
471 """
472 return self._resumable
473
474 def getbytes(self, begin, length):
475 """Get bytes from the media.
476
477 Args:
478 begin: int, offset from beginning of file.
479 length: int, number of bytes to read, starting at begin.
480
481 Returns:
482 A string of bytes read. May be shorted than length if EOF was reached
483 first.
484 """
485 self._fd.seek(begin)
486 return self._fd.read(length)
487
488 def has_stream(self):
489 """Does the underlying upload support a streaming interface.
490
491 Streaming means it is an io.IOBase subclass that supports seek, i.e.
492 seekable() returns True.
493
494 Returns:
495 True if the call to stream() will return an instance of a seekable io.Base
496 subclass.
497 """
498 return True
499
500 def stream(self):
501 """A stream interface to the data being uploaded.
502
503 Returns:
504 The returned value is an io.IOBase subclass that supports seek, i.e.
505 seekable() returns True.
506 """
507 return self._fd
508
509 def to_json(self):
510 """This upload type is not serializable."""
511 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
512
513
514class MediaFileUpload(MediaIoBaseUpload):
515 """A MediaUpload for a file.
516
517 Construct a MediaFileUpload and pass as the media_body parameter of the
518 method. For example, if we had a service that allowed uploading images:
519
John Asmuth864311d2014-04-24 15:46:08 -0400520 media = MediaFileUpload('cow.png', mimetype='image/png',
521 chunksize=1024*1024, resumable=True)
522 farm.animals().insert(
523 id='cow',
524 name='cow.png',
525 media_body=media).execute()
526
527 Depending on the platform you are working on, you may pass -1 as the
528 chunksize, which indicates that the entire file should be uploaded in a single
529 request. If the underlying platform supports streams, such as Python 2.6 or
530 later, then this can be very efficient as it avoids multiple connections, and
531 also avoids loading the entire file into memory before sending it. Note that
532 Google App Engine has a 5MB limit on request size, so you should never set
533 your chunksize larger than 5MB, or to -1.
534 """
535
536 @util.positional(2)
537 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
538 resumable=False):
539 """Constructor.
540
541 Args:
542 filename: string, Name of the file.
543 mimetype: string, Mime-type of the file. If None then a mime-type will be
544 guessed from the file extension.
545 chunksize: int, File will be uploaded in chunks of this many bytes. Only
546 used if resumable=True. Pass in a value of -1 if the file is to be
547 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
548 on request size, so you should never set your chunksize larger than 5MB,
549 or to -1.
550 resumable: bool, True if this is a resumable upload. False means upload
551 in a single request.
552 """
553 self._filename = filename
554 fd = open(self._filename, 'rb')
555 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800556 # No mimetype provided, make a guess.
557 mimetype, _ = mimetypes.guess_type(filename)
558 if mimetype is None:
559 # Guess failed, use octet-stream.
560 mimetype = 'application/octet-stream'
John Asmuth864311d2014-04-24 15:46:08 -0400561 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
562 resumable=resumable)
563
564 def to_json(self):
565 """Creating a JSON representation of an instance of MediaFileUpload.
566
567 Returns:
568 string, a JSON representation of this instance, suitable to pass to
569 from_json().
570 """
571 return self._to_json(strip=['_fd'])
572
573 @staticmethod
574 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700575 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400576 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
577 chunksize=d['_chunksize'], resumable=d['_resumable'])
578
579
580class MediaInMemoryUpload(MediaIoBaseUpload):
581 """MediaUpload for a chunk of bytes.
582
583 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
584 the stream.
585 """
586
587 @util.positional(2)
588 def __init__(self, body, mimetype='application/octet-stream',
589 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
590 """Create a new MediaInMemoryUpload.
591
592 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
593 the stream.
594
595 Args:
596 body: string, Bytes of body content.
597 mimetype: string, Mime-type of the file or default of
598 'application/octet-stream'.
599 chunksize: int, File will be uploaded in chunks of this many bytes. Only
600 used if resumable=True.
601 resumable: bool, True if this is a resumable upload. False means upload
602 in a single request.
603 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800604 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400605 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
606 resumable=resumable)
607
608
609class MediaIoBaseDownload(object):
610 """"Download media resources.
611
612 Note that the Python file object is compatible with io.Base and can be used
613 with this class also.
614
615
616 Example:
617 request = farms.animals().get_media(id='cow')
618 fh = io.FileIO('cow.png', mode='wb')
619 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
620
621 done = False
622 while done is False:
623 status, done = downloader.next_chunk()
624 if status:
625 print "Download %d%%." % int(status.progress() * 100)
626 print "Download Complete!"
627 """
628
629 @util.positional(3)
630 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
631 """Constructor.
632
633 Args:
634 fd: io.Base or file object, The stream in which to write the downloaded
635 bytes.
636 request: googleapiclient.http.HttpRequest, the media request to perform in
637 chunks.
638 chunksize: int, File will be downloaded in chunks of this many bytes.
639 """
640 self._fd = fd
641 self._request = request
642 self._uri = request.uri
643 self._chunksize = chunksize
644 self._progress = 0
645 self._total_size = None
646 self._done = False
647
648 # Stubs for testing.
649 self._sleep = time.sleep
650 self._rand = random.random
651
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400652 self._headers = {}
653 for k, v in six.iteritems(request.headers):
654 # allow users to supply custom headers by setting them on the request
655 # but strip out the ones that are set by default on requests generated by
656 # API methods like Drive's files().get(fileId=...)
657 if not k.lower() in ('accept', 'accept-encoding', 'user-agent'):
658 self._headers[k] = v
659
John Asmuth864311d2014-04-24 15:46:08 -0400660 @util.positional(1)
661 def next_chunk(self, num_retries=0):
662 """Get the next chunk of the download.
663
664 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500665 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400666 exponential backoff. If all retries fail, the raised HttpError
667 represents the last request. If zero (default), we attempt the
668 request only once.
669
670 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700671 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400672 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100673 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400674
675 Raises:
676 googleapiclient.errors.HttpError if the response was not a 2xx.
677 httplib2.HttpLib2Error if a transport error has occured.
678 """
Chris McDonough0dc81bf2018-07-19 11:19:58 -0400679 headers = self._headers.copy()
680 headers['range'] = 'bytes=%d-%d' % (
John Asmuth864311d2014-04-24 15:46:08 -0400681 self._progress, self._progress + self._chunksize)
John Asmuth864311d2014-04-24 15:46:08 -0400682 http = self._request.http
683
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100684 resp, content = _retry_request(
685 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
686 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400687
688 if resp.status in [200, 206]:
689 if 'content-location' in resp and resp['content-location'] != self._uri:
690 self._uri = resp['content-location']
691 self._progress += len(content)
692 self._fd.write(content)
693
694 if 'content-range' in resp:
695 content_range = resp['content-range']
696 length = content_range.rsplit('/', 1)[1]
697 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800698 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800699 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400700
Daniel44067782018-01-16 23:17:56 +0100701 if self._total_size is None or self._progress == self._total_size:
John Asmuth864311d2014-04-24 15:46:08 -0400702 self._done = True
703 return MediaDownloadProgress(self._progress, self._total_size), self._done
704 else:
705 raise HttpError(resp, content, uri=self._uri)
706
707
708class _StreamSlice(object):
709 """Truncated stream.
710
711 Takes a stream and presents a stream that is a slice of the original stream.
712 This is used when uploading media in chunks. In later versions of Python a
713 stream can be passed to httplib in place of the string of data to send. The
714 problem is that httplib just blindly reads to the end of the stream. This
715 wrapper presents a virtual stream that only reads to the end of the chunk.
716 """
717
718 def __init__(self, stream, begin, chunksize):
719 """Constructor.
720
721 Args:
722 stream: (io.Base, file object), the stream to wrap.
723 begin: int, the seek position the chunk begins at.
724 chunksize: int, the size of the chunk.
725 """
726 self._stream = stream
727 self._begin = begin
728 self._chunksize = chunksize
729 self._stream.seek(begin)
730
731 def read(self, n=-1):
732 """Read n bytes.
733
734 Args:
735 n, int, the number of bytes to read.
736
737 Returns:
738 A string of length 'n', or less if EOF is reached.
739 """
740 # The data left available to read sits in [cur, end)
741 cur = self._stream.tell()
742 end = self._begin + self._chunksize
743 if n == -1 or cur + n > end:
744 n = end - cur
745 return self._stream.read(n)
746
747
748class HttpRequest(object):
749 """Encapsulates a single HTTP request."""
750
751 @util.positional(4)
752 def __init__(self, http, postproc, uri,
753 method='GET',
754 body=None,
755 headers=None,
756 methodId=None,
757 resumable=None):
758 """Constructor for an HttpRequest.
759
760 Args:
761 http: httplib2.Http, the transport object to use to make a request
762 postproc: callable, called on the HTTP response and content to transform
763 it into a data object before returning, or raising an exception
764 on an error.
765 uri: string, the absolute URI to send the request to
766 method: string, the HTTP method to use
767 body: string, the request body of the HTTP request,
768 headers: dict, the HTTP request headers
769 methodId: string, a unique identifier for the API method being called.
770 resumable: MediaUpload, None if this is not a resumbale request.
771 """
772 self.uri = uri
773 self.method = method
774 self.body = body
775 self.headers = headers or {}
776 self.methodId = methodId
777 self.http = http
778 self.postproc = postproc
779 self.resumable = resumable
780 self.response_callbacks = []
781 self._in_error_state = False
782
John Asmuth864311d2014-04-24 15:46:08 -0400783 # The size of the non-media part of the request.
784 self.body_size = len(self.body or '')
785
786 # The resumable URI to send chunks to.
787 self.resumable_uri = None
788
789 # The bytes that have been uploaded.
790 self.resumable_progress = 0
791
792 # Stubs for testing.
793 self._rand = random.random
794 self._sleep = time.sleep
795
796 @util.positional(1)
797 def execute(self, http=None, num_retries=0):
798 """Execute the request.
799
800 Args:
801 http: httplib2.Http, an http object to be used in place of the
802 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500803 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400804 exponential backoff. If all retries fail, the raised HttpError
805 represents the last request. If zero (default), we attempt the
806 request only once.
807
808 Returns:
809 A deserialized object model of the response body as determined
810 by the postproc.
811
812 Raises:
813 googleapiclient.errors.HttpError if the response was not a 2xx.
814 httplib2.HttpLib2Error if a transport error has occured.
815 """
816 if http is None:
817 http = self.http
818
819 if self.resumable:
820 body = None
821 while body is None:
822 _, body = self.next_chunk(http=http, num_retries=num_retries)
823 return body
824
825 # Non-resumable case.
826
827 if 'content-length' not in self.headers:
828 self.headers['content-length'] = str(self.body_size)
829 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800830 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400831 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
832 self.method = 'POST'
833 self.headers['x-http-method-override'] = 'GET'
834 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800835 parsed = urlparse(self.uri)
836 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400837 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
838 None)
839 )
840 self.body = parsed.query
841 self.headers['content-length'] = str(len(self.body))
842
843 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100844 resp, content = _retry_request(
845 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
846 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400847
848 for callback in self.response_callbacks:
849 callback(resp)
850 if resp.status >= 300:
851 raise HttpError(resp, content, uri=self.uri)
852 return self.postproc(resp, content)
853
854 @util.positional(2)
855 def add_response_callback(self, cb):
856 """add_response_headers_callback
857
858 Args:
859 cb: Callback to be called on receiving the response headers, of signature:
860
861 def cb(resp):
862 # Where resp is an instance of httplib2.Response
863 """
864 self.response_callbacks.append(cb)
865
866 @util.positional(1)
867 def next_chunk(self, http=None, num_retries=0):
868 """Execute the next step of a resumable upload.
869
870 Can only be used if the method being executed supports media uploads and
871 the MediaUpload object passed in was flagged as using resumable upload.
872
873 Example:
874
875 media = MediaFileUpload('cow.png', mimetype='image/png',
876 chunksize=1000, resumable=True)
877 request = farm.animals().insert(
878 id='cow',
879 name='cow.png',
880 media_body=media)
881
882 response = None
883 while response is None:
884 status, response = request.next_chunk()
885 if status:
886 print "Upload %d%% complete." % int(status.progress() * 100)
887
888
889 Args:
890 http: httplib2.Http, an http object to be used in place of the
891 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500892 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400893 exponential backoff. If all retries fail, the raised HttpError
894 represents the last request. If zero (default), we attempt the
895 request only once.
896
897 Returns:
898 (status, body): (ResumableMediaStatus, object)
899 The body will be None until the resumable media is fully uploaded.
900
901 Raises:
902 googleapiclient.errors.HttpError if the response was not a 2xx.
903 httplib2.HttpLib2Error if a transport error has occured.
904 """
905 if http is None:
906 http = self.http
907
908 if self.resumable.size() is None:
909 size = '*'
910 else:
911 size = str(self.resumable.size())
912
913 if self.resumable_uri is None:
914 start_headers = copy.copy(self.headers)
915 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
916 if size != '*':
917 start_headers['X-Upload-Content-Length'] = size
918 start_headers['content-length'] = str(self.body_size)
919
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100920 resp, content = _retry_request(
921 http, num_retries, 'resumable URI request', self._sleep, self._rand,
922 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400923
924 if resp.status == 200 and 'location' in resp:
925 self.resumable_uri = resp['location']
926 else:
927 raise ResumableUploadError(resp, content)
928 elif self._in_error_state:
929 # If we are in an error state then query the server for current state of
930 # the upload by sending an empty PUT and reading the 'range' header in
931 # the response.
932 headers = {
933 'Content-Range': 'bytes */%s' % size,
934 'content-length': '0'
935 }
936 resp, content = http.request(self.resumable_uri, 'PUT',
937 headers=headers)
938 status, body = self._process_response(resp, content)
939 if body:
940 # The upload was complete.
941 return (status, body)
942
e00Efafe8582015-10-10 18:19:37 +0200943 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400944 data = self.resumable.stream()
945 if self.resumable.chunksize() == -1:
946 data.seek(self.resumable_progress)
947 chunk_end = self.resumable.size() - self.resumable_progress - 1
948 else:
949 # Doing chunking with a stream, so wrap a slice of the stream.
950 data = _StreamSlice(data, self.resumable_progress,
951 self.resumable.chunksize())
952 chunk_end = min(
953 self.resumable_progress + self.resumable.chunksize() - 1,
954 self.resumable.size() - 1)
955 else:
956 data = self.resumable.getbytes(
957 self.resumable_progress, self.resumable.chunksize())
958
959 # A short read implies that we are at EOF, so finish the upload.
960 if len(data) < self.resumable.chunksize():
961 size = str(self.resumable_progress + len(data))
962
963 chunk_end = self.resumable_progress + len(data) - 1
964
965 headers = {
966 'Content-Range': 'bytes %d-%d/%s' % (
967 self.resumable_progress, chunk_end, size),
968 # Must set the content-length header here because httplib can't
969 # calculate the size when working with _StreamSlice.
970 'Content-Length': str(chunk_end - self.resumable_progress + 1)
971 }
972
INADA Naokie4ea1a92015-03-04 03:45:42 +0900973 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400974 if retry_num > 0:
975 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800976 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400977 'Retry #%d for media upload: %s %s, following status: %d'
978 % (retry_num, self.method, self.uri, resp.status))
979
980 try:
981 resp, content = http.request(self.resumable_uri, method='PUT',
982 body=data,
983 headers=headers)
984 except:
985 self._in_error_state = True
986 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500987 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400988 break
989
990 return self._process_response(resp, content)
991
992 def _process_response(self, resp, content):
993 """Process the response from a single chunk upload.
994
995 Args:
996 resp: httplib2.Response, the response object.
997 content: string, the content of the response.
998
999 Returns:
1000 (status, body): (ResumableMediaStatus, object)
1001 The body will be None until the resumable media is fully uploaded.
1002
1003 Raises:
1004 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
1005 """
1006 if resp.status in [200, 201]:
1007 self._in_error_state = False
1008 return None, self.postproc(resp, content)
1009 elif resp.status == 308:
1010 self._in_error_state = False
1011 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001012 try:
1013 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1014 except KeyError:
1015 # If resp doesn't contain range header, resumable progress is 0
1016 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001017 if 'location' in resp:
1018 self.resumable_uri = resp['location']
1019 else:
1020 self._in_error_state = True
1021 raise HttpError(resp, content, uri=self.uri)
1022
1023 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1024 None)
1025
1026 def to_json(self):
1027 """Returns a JSON representation of the HttpRequest."""
1028 d = copy.copy(self.__dict__)
1029 if d['resumable'] is not None:
1030 d['resumable'] = self.resumable.to_json()
1031 del d['http']
1032 del d['postproc']
1033 del d['_sleep']
1034 del d['_rand']
1035
Craig Citro6ae34d72014-08-18 23:10:09 -07001036 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001037
1038 @staticmethod
1039 def from_json(s, http, postproc):
1040 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001041 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001042 if d['resumable'] is not None:
1043 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1044 return HttpRequest(
1045 http,
1046 postproc,
1047 uri=d['uri'],
1048 method=d['method'],
1049 body=d['body'],
1050 headers=d['headers'],
1051 methodId=d['methodId'],
1052 resumable=d['resumable'])
1053
1054
1055class BatchHttpRequest(object):
1056 """Batches multiple HttpRequest objects into a single HTTP request.
1057
1058 Example:
1059 from googleapiclient.http import BatchHttpRequest
1060
1061 def list_animals(request_id, response, exception):
1062 \"\"\"Do something with the animals list response.\"\"\"
1063 if exception is not None:
1064 # Do something with the exception.
1065 pass
1066 else:
1067 # Do something with the response.
1068 pass
1069
1070 def list_farmers(request_id, response, exception):
1071 \"\"\"Do something with the farmers list response.\"\"\"
1072 if exception is not None:
1073 # Do something with the exception.
1074 pass
1075 else:
1076 # Do something with the response.
1077 pass
1078
1079 service = build('farm', 'v2')
1080
1081 batch = BatchHttpRequest()
1082
1083 batch.add(service.animals().list(), list_animals)
1084 batch.add(service.farmers().list(), list_farmers)
1085 batch.execute(http=http)
1086 """
1087
1088 @util.positional(1)
1089 def __init__(self, callback=None, batch_uri=None):
1090 """Constructor for a BatchHttpRequest.
1091
1092 Args:
1093 callback: callable, A callback to be called for each response, of the
1094 form callback(id, response, exception). The first parameter is the
1095 request id, and the second is the deserialized response object. The
1096 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1097 occurred while processing the request, or None if no error occurred.
1098 batch_uri: string, URI to send batch requests to.
1099 """
1100 if batch_uri is None:
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001101 batch_uri = _LEGACY_BATCH_URI
1102
1103 if batch_uri == _LEGACY_BATCH_URI:
1104 LOGGER.warn(
1105 "You have constructed a BatchHttpRequest using the legacy batch "
1106 "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1107 "Please provide the API-specific endpoint or use "
1108 "service.new_batch_http_request(). For more details see "
1109 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1110 "and https://developers.google.com/api-client-library/python/guide/batch.",
1111 _LEGACY_BATCH_URI)
John Asmuth864311d2014-04-24 15:46:08 -04001112 self._batch_uri = batch_uri
1113
1114 # Global callback to be called for each individual response in the batch.
1115 self._callback = callback
1116
1117 # A map from id to request.
1118 self._requests = {}
1119
1120 # A map from id to callback.
1121 self._callbacks = {}
1122
1123 # List of request ids, in the order in which they were added.
1124 self._order = []
1125
1126 # The last auto generated id.
1127 self._last_auto_id = 0
1128
1129 # Unique ID on which to base the Content-ID headers.
1130 self._base_id = None
1131
1132 # A map from request id to (httplib2.Response, content) response pairs
1133 self._responses = {}
1134
1135 # A map of id(Credentials) that have been refreshed.
1136 self._refreshed_credentials = {}
1137
1138 def _refresh_and_apply_credentials(self, request, http):
1139 """Refresh the credentials and apply to the request.
1140
1141 Args:
1142 request: HttpRequest, the request.
1143 http: httplib2.Http, the global http object for the batch.
1144 """
1145 # For the credentials to refresh, but only once per refresh_token
1146 # If there is no http per the request then refresh the http passed in
1147 # via execute()
1148 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001149 request_credentials = False
1150
1151 if request.http is not None:
1152 creds = _auth.get_credentials_from_http(request.http)
1153 request_credentials = True
1154
1155 if creds is None and http is not None:
1156 creds = _auth.get_credentials_from_http(http)
1157
John Asmuth864311d2014-04-24 15:46:08 -04001158 if creds is not None:
1159 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001160 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001161 self._refreshed_credentials[id(creds)] = 1
1162
1163 # Only apply the credentials if we are using the http object passed in,
1164 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001165 if request.http is None or not request_credentials:
1166 _auth.apply_credentials(creds, request.headers)
1167
John Asmuth864311d2014-04-24 15:46:08 -04001168
1169 def _id_to_header(self, id_):
1170 """Convert an id to a Content-ID header value.
1171
1172 Args:
1173 id_: string, identifier of individual request.
1174
1175 Returns:
1176 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1177 the value because Content-ID headers are supposed to be universally
1178 unique.
1179 """
1180 if self._base_id is None:
1181 self._base_id = uuid.uuid4()
1182
Chris McDonough3cf5e602018-07-18 16:18:38 -04001183 # NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1184 # line folding works properly on Python 3; see
1185 # https://github.com/google/google-api-python-client/issues/164
1186 return '<%s + %s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001187
1188 def _header_to_id(self, header):
1189 """Convert a Content-ID header value to an id.
1190
1191 Presumes the Content-ID header conforms to the format that _id_to_header()
1192 returns.
1193
1194 Args:
1195 header: string, Content-ID header value.
1196
1197 Returns:
1198 The extracted id value.
1199
1200 Raises:
1201 BatchError if the header is not in the expected format.
1202 """
1203 if header[0] != '<' or header[-1] != '>':
1204 raise BatchError("Invalid value for Content-ID: %s" % header)
1205 if '+' not in header:
1206 raise BatchError("Invalid value for Content-ID: %s" % header)
Chris McDonough3cf5e602018-07-18 16:18:38 -04001207 base, id_ = header[1:-1].split(' + ', 1)
John Asmuth864311d2014-04-24 15:46:08 -04001208
Pat Ferated5b61bd2015-03-03 16:04:11 -08001209 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001210
1211 def _serialize_request(self, request):
1212 """Convert an HttpRequest object into a string.
1213
1214 Args:
1215 request: HttpRequest, the request to serialize.
1216
1217 Returns:
1218 The request as a string in application/http format.
1219 """
1220 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001221 parsed = urlparse(request.uri)
1222 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001223 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001224 )
1225 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1226 major, minor = request.headers.get('content-type', 'application/json').split('/')
1227 msg = MIMENonMultipart(major, minor)
1228 headers = request.headers.copy()
1229
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001230 if request.http is not None:
1231 credentials = _auth.get_credentials_from_http(request.http)
1232 if credentials is not None:
1233 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001234
1235 # MIMENonMultipart adds its own Content-Type header.
1236 if 'content-type' in headers:
1237 del headers['content-type']
1238
INADA Naokie4ea1a92015-03-04 03:45:42 +09001239 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001240 msg[key] = value
1241 msg['Host'] = parsed.netloc
1242 msg.set_unixfrom(None)
1243
1244 if request.body is not None:
1245 msg.set_payload(request.body)
1246 msg['content-length'] = str(len(request.body))
1247
1248 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001249 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001250 # maxheaderlen=0 means don't line wrap headers.
1251 g = Generator(fp, maxheaderlen=0)
1252 g.flatten(msg, unixfrom=False)
1253 body = fp.getvalue()
1254
Pat Feratec9abbbd2015-03-03 18:00:38 -08001255 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001256
1257 def _deserialize_response(self, payload):
1258 """Convert string into httplib2 response and content.
1259
1260 Args:
1261 payload: string, headers and body as a string.
1262
1263 Returns:
1264 A pair (resp, content), such as would be returned from httplib2.request.
1265 """
1266 # Strip off the status line
1267 status_line, payload = payload.split('\n', 1)
1268 protocol, status, reason = status_line.split(' ', 2)
1269
1270 # Parse the rest of the response
1271 parser = FeedParser()
1272 parser.feed(payload)
1273 msg = parser.close()
1274 msg['status'] = status
1275
1276 # Create httplib2.Response from the parsed headers.
1277 resp = httplib2.Response(msg)
1278 resp.reason = reason
1279 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1280
1281 content = payload.split('\r\n\r\n', 1)[1]
1282
1283 return resp, content
1284
1285 def _new_id(self):
1286 """Create a new id.
1287
1288 Auto incrementing number that avoids conflicts with ids already used.
1289
1290 Returns:
1291 string, a new unique id.
1292 """
1293 self._last_auto_id += 1
1294 while str(self._last_auto_id) in self._requests:
1295 self._last_auto_id += 1
1296 return str(self._last_auto_id)
1297
1298 @util.positional(2)
1299 def add(self, request, callback=None, request_id=None):
1300 """Add a new request.
1301
1302 Every callback added will be paired with a unique id, the request_id. That
1303 unique id will be passed back to the callback when the response comes back
1304 from the server. The default behavior is to have the library generate it's
1305 own unique id. If the caller passes in a request_id then they must ensure
1306 uniqueness for each request_id, and if they are not an exception is
cspeidelfbaf9d72018-05-10 12:50:12 -06001307 raised. Callers should either supply all request_ids or never supply a
John Asmuth864311d2014-04-24 15:46:08 -04001308 request id, to avoid such an error.
1309
1310 Args:
1311 request: HttpRequest, Request to add to the batch.
1312 callback: callable, A callback to be called for this response, of the
1313 form callback(id, response, exception). The first parameter is the
1314 request id, and the second is the deserialized response object. The
1315 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1316 occurred while processing the request, or None if no errors occurred.
Chris McDonough3cf5e602018-07-18 16:18:38 -04001317 request_id: string, A unique id for the request. The id will be passed
1318 to the callback with the response.
John Asmuth864311d2014-04-24 15:46:08 -04001319
1320 Returns:
1321 None
1322
1323 Raises:
1324 BatchError if a media request is added to a batch.
1325 KeyError is the request_id is not unique.
1326 """
Xinan Line2dccec2018-12-07 05:28:33 +09001327
1328 if len(self._order) >= MAX_BATCH_LIMIT:
smstonea04b3c52018-12-10 14:13:41 -05001329 raise BatchError("Exceeded the maximum calls(%d) in a single batch request."
Xinan Line2dccec2018-12-07 05:28:33 +09001330 % MAX_BATCH_LIMIT)
John Asmuth864311d2014-04-24 15:46:08 -04001331 if request_id is None:
1332 request_id = self._new_id()
1333 if request.resumable is not None:
1334 raise BatchError("Media requests cannot be used in a batch request.")
1335 if request_id in self._requests:
1336 raise KeyError("A request with this ID already exists: %s" % request_id)
1337 self._requests[request_id] = request
1338 self._callbacks[request_id] = callback
1339 self._order.append(request_id)
1340
1341 def _execute(self, http, order, requests):
1342 """Serialize batch request, send to server, process response.
1343
1344 Args:
1345 http: httplib2.Http, an http object to be used to make the request with.
1346 order: list, list of request ids in the order they were added to the
1347 batch.
1348 request: list, list of request objects to send.
1349
1350 Raises:
1351 httplib2.HttpLib2Error if a transport error has occured.
1352 googleapiclient.errors.BatchError if the response is the wrong format.
1353 """
1354 message = MIMEMultipart('mixed')
1355 # Message should not write out it's own headers.
1356 setattr(message, '_write_headers', lambda self: None)
1357
1358 # Add all the individual requests.
1359 for request_id in order:
1360 request = requests[request_id]
1361
1362 msg = MIMENonMultipart('application', 'http')
1363 msg['Content-Transfer-Encoding'] = 'binary'
1364 msg['Content-ID'] = self._id_to_header(request_id)
1365
1366 body = self._serialize_request(request)
1367 msg.set_payload(body)
1368 message.attach(msg)
1369
Craig Citro72389b72014-07-15 17:12:50 -07001370 # encode the body: note that we can't use `as_string`, because
1371 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001372 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001373 g = Generator(fp, mangle_from_=False)
1374 g.flatten(message, unixfrom=False)
1375 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001376
1377 headers = {}
1378 headers['content-type'] = ('multipart/mixed; '
1379 'boundary="%s"') % message.get_boundary()
1380
1381 resp, content = http.request(self._batch_uri, method='POST', body=body,
1382 headers=headers)
1383
1384 if resp.status >= 300:
1385 raise HttpError(resp, content, uri=self._batch_uri)
1386
John Asmuth864311d2014-04-24 15:46:08 -04001387 # Prepend with a content-type header so FeedParser can handle it.
1388 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001389 # PY3's FeedParser only accepts unicode. So we should decode content
1390 # here, and encode each payload again.
1391 if six.PY3:
1392 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001393 for_parser = header + content
1394
1395 parser = FeedParser()
1396 parser.feed(for_parser)
1397 mime_response = parser.close()
1398
1399 if not mime_response.is_multipart():
1400 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1401 content=content)
1402
1403 for part in mime_response.get_payload():
1404 request_id = self._header_to_id(part['Content-ID'])
1405 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001406 # We encode content here to emulate normal http response.
1407 if isinstance(content, six.text_type):
1408 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001409 self._responses[request_id] = (response, content)
1410
1411 @util.positional(1)
1412 def execute(self, http=None):
1413 """Execute all the requests as a single batched HTTP request.
1414
1415 Args:
1416 http: httplib2.Http, an http object to be used in place of the one the
1417 HttpRequest request object was constructed with. If one isn't supplied
1418 then use a http object from the requests in this batch.
1419
1420 Returns:
1421 None
1422
1423 Raises:
1424 httplib2.HttpLib2Error if a transport error has occured.
1425 googleapiclient.errors.BatchError if the response is the wrong format.
1426 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001427 # If we have no requests return
1428 if len(self._order) == 0:
1429 return None
John Asmuth864311d2014-04-24 15:46:08 -04001430
1431 # If http is not supplied use the first valid one given in the requests.
1432 if http is None:
1433 for request_id in self._order:
1434 request = self._requests[request_id]
1435 if request is not None:
1436 http = request.http
1437 break
1438
1439 if http is None:
1440 raise ValueError("Missing a valid http object.")
1441
Gabriel Garcia23174be2016-05-25 17:28:07 +02001442 # Special case for OAuth2Credentials-style objects which have not yet been
1443 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001444 creds = _auth.get_credentials_from_http(http)
1445 if creds is not None:
1446 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001447 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001448 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001449
John Asmuth864311d2014-04-24 15:46:08 -04001450 self._execute(http, self._order, self._requests)
1451
1452 # Loop over all the requests and check for 401s. For each 401 request the
1453 # credentials should be refreshed and then sent again in a separate batch.
1454 redo_requests = {}
1455 redo_order = []
1456
1457 for request_id in self._order:
1458 resp, content = self._responses[request_id]
1459 if resp['status'] == '401':
1460 redo_order.append(request_id)
1461 request = self._requests[request_id]
1462 self._refresh_and_apply_credentials(request, http)
1463 redo_requests[request_id] = request
1464
1465 if redo_requests:
1466 self._execute(http, redo_order, redo_requests)
1467
1468 # Now process all callbacks that are erroring, and raise an exception for
1469 # ones that return a non-2xx response? Or add extra parameter to callback
1470 # that contains an HttpError?
1471
1472 for request_id in self._order:
1473 resp, content = self._responses[request_id]
1474
1475 request = self._requests[request_id]
1476 callback = self._callbacks[request_id]
1477
1478 response = None
1479 exception = None
1480 try:
1481 if resp.status >= 300:
1482 raise HttpError(resp, content, uri=request.uri)
1483 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001484 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001485 exception = e
1486
1487 if callback is not None:
1488 callback(request_id, response, exception)
1489 if self._callback is not None:
1490 self._callback(request_id, response, exception)
1491
1492
1493class HttpRequestMock(object):
1494 """Mock of HttpRequest.
1495
1496 Do not construct directly, instead use RequestMockBuilder.
1497 """
1498
1499 def __init__(self, resp, content, postproc):
1500 """Constructor for HttpRequestMock
1501
1502 Args:
1503 resp: httplib2.Response, the response to emulate coming from the request
1504 content: string, the response body
1505 postproc: callable, the post processing function usually supplied by
1506 the model class. See model.JsonModel.response() as an example.
1507 """
1508 self.resp = resp
1509 self.content = content
1510 self.postproc = postproc
1511 if resp is None:
1512 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1513 if 'reason' in self.resp:
1514 self.resp.reason = self.resp['reason']
1515
1516 def execute(self, http=None):
1517 """Execute the request.
1518
1519 Same behavior as HttpRequest.execute(), but the response is
1520 mocked and not really from an HTTP request/response.
1521 """
1522 return self.postproc(self.resp, self.content)
1523
1524
1525class RequestMockBuilder(object):
1526 """A simple mock of HttpRequest
1527
1528 Pass in a dictionary to the constructor that maps request methodIds to
1529 tuples of (httplib2.Response, content, opt_expected_body) that should be
1530 returned when that method is called. None may also be passed in for the
1531 httplib2.Response, in which case a 200 OK response will be generated.
1532 If an opt_expected_body (str or dict) is provided, it will be compared to
1533 the body and UnexpectedBodyError will be raised on inequality.
1534
1535 Example:
1536 response = '{"data": {"id": "tag:google.c...'
1537 requestBuilder = RequestMockBuilder(
1538 {
1539 'plus.activities.get': (None, response),
1540 }
1541 )
1542 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1543
1544 Methods that you do not supply a response for will return a
1545 200 OK with an empty string as the response content or raise an excpetion
1546 if check_unexpected is set to True. The methodId is taken from the rpcName
1547 in the discovery document.
1548
1549 For more details see the project wiki.
1550 """
1551
1552 def __init__(self, responses, check_unexpected=False):
1553 """Constructor for RequestMockBuilder
1554
1555 The constructed object should be a callable object
1556 that can replace the class HttpResponse.
1557
1558 responses - A dictionary that maps methodIds into tuples
1559 of (httplib2.Response, content). The methodId
1560 comes from the 'rpcName' field in the discovery
1561 document.
1562 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1563 should be raised on unsupplied method.
1564 """
1565 self.responses = responses
1566 self.check_unexpected = check_unexpected
1567
1568 def __call__(self, http, postproc, uri, method='GET', body=None,
1569 headers=None, methodId=None, resumable=None):
1570 """Implements the callable interface that discovery.build() expects
1571 of requestBuilder, which is to build an object compatible with
1572 HttpRequest.execute(). See that method for the description of the
1573 parameters and the expected response.
1574 """
1575 if methodId in self.responses:
1576 response = self.responses[methodId]
1577 resp, content = response[:2]
1578 if len(response) > 2:
1579 # Test the body against the supplied expected_body.
1580 expected_body = response[2]
1581 if bool(expected_body) != bool(body):
1582 # Not expecting a body and provided one
1583 # or expecting a body and not provided one.
1584 raise UnexpectedBodyError(expected_body, body)
1585 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001586 expected_body = json.loads(expected_body)
1587 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001588 if body != expected_body:
1589 raise UnexpectedBodyError(expected_body, body)
1590 return HttpRequestMock(resp, content, postproc)
1591 elif self.check_unexpected:
1592 raise UnexpectedMethodError(methodId=methodId)
1593 else:
1594 model = JsonModel(False)
1595 return HttpRequestMock(None, '{}', model.response)
1596
1597
1598class HttpMock(object):
1599 """Mock of httplib2.Http"""
1600
1601 def __init__(self, filename=None, headers=None):
1602 """
1603 Args:
1604 filename: string, absolute filename to read response from
1605 headers: dict, header to return with response
1606 """
1607 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001608 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001609 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001610 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001611 self.data = f.read()
1612 f.close()
1613 else:
1614 self.data = None
1615 self.response_headers = headers
1616 self.headers = None
1617 self.uri = None
1618 self.method = None
1619 self.body = None
1620 self.headers = None
1621
1622
1623 def request(self, uri,
1624 method='GET',
1625 body=None,
1626 headers=None,
1627 redirections=1,
1628 connection_type=None):
1629 self.uri = uri
1630 self.method = method
1631 self.body = body
1632 self.headers = headers
1633 return httplib2.Response(self.response_headers), self.data
1634
1635
1636class HttpMockSequence(object):
1637 """Mock of httplib2.Http
1638
1639 Mocks a sequence of calls to request returning different responses for each
1640 call. Create an instance initialized with the desired response headers
1641 and content and then use as if an httplib2.Http instance.
1642
1643 http = HttpMockSequence([
1644 ({'status': '401'}, ''),
1645 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1646 ({'status': '200'}, 'echo_request_headers'),
1647 ])
1648 resp, content = http.request("http://examples.com")
1649
1650 There are special values you can pass in for content to trigger
1651 behavours that are helpful in testing.
1652
1653 'echo_request_headers' means return the request headers in the response body
1654 'echo_request_headers_as_json' means return the request headers in
1655 the response body
1656 'echo_request_body' means return the request body in the response body
1657 'echo_request_uri' means return the request uri in the response body
1658 """
1659
1660 def __init__(self, iterable):
1661 """
1662 Args:
1663 iterable: iterable, a sequence of pairs of (headers, body)
1664 """
1665 self._iterable = iterable
1666 self.follow_redirects = True
1667
1668 def request(self, uri,
1669 method='GET',
1670 body=None,
1671 headers=None,
1672 redirections=1,
1673 connection_type=None):
1674 resp, content = self._iterable.pop(0)
1675 if content == 'echo_request_headers':
1676 content = headers
1677 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001678 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001679 elif content == 'echo_request_body':
1680 if hasattr(body, 'read'):
1681 content = body.read()
1682 else:
1683 content = body
1684 elif content == 'echo_request_uri':
1685 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001686 if isinstance(content, six.text_type):
1687 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001688 return httplib2.Response(resp), content
1689
1690
1691def set_user_agent(http, user_agent):
1692 """Set the user-agent on every request.
1693
1694 Args:
1695 http - An instance of httplib2.Http
1696 or something that acts like it.
1697 user_agent: string, the value for the user-agent header.
1698
1699 Returns:
1700 A modified instance of http that was passed in.
1701
1702 Example:
1703
1704 h = httplib2.Http()
1705 h = set_user_agent(h, "my-app-name/6.0")
1706
1707 Most of the time the user-agent will be set doing auth, this is for the rare
1708 cases where you are accessing an unauthenticated endpoint.
1709 """
1710 request_orig = http.request
1711
1712 # The closure that will replace 'httplib2.Http.request'.
1713 def new_request(uri, method='GET', body=None, headers=None,
1714 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1715 connection_type=None):
1716 """Modify the request headers to add the user-agent."""
1717 if headers is None:
1718 headers = {}
1719 if 'user-agent' in headers:
1720 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1721 else:
1722 headers['user-agent'] = user_agent
1723 resp, content = request_orig(uri, method, body, headers,
1724 redirections, connection_type)
1725 return resp, content
1726
1727 http.request = new_request
1728 return http
1729
1730
1731def tunnel_patch(http):
1732 """Tunnel PATCH requests over POST.
1733 Args:
1734 http - An instance of httplib2.Http
1735 or something that acts like it.
1736
1737 Returns:
1738 A modified instance of http that was passed in.
1739
1740 Example:
1741
1742 h = httplib2.Http()
1743 h = tunnel_patch(h, "my-app-name/6.0")
1744
1745 Useful if you are running on a platform that doesn't support PATCH.
1746 Apply this last if you are using OAuth 1.0, as changing the method
1747 will result in a different signature.
1748 """
1749 request_orig = http.request
1750
1751 # The closure that will replace 'httplib2.Http.request'.
1752 def new_request(uri, method='GET', body=None, headers=None,
1753 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1754 connection_type=None):
1755 """Modify the request headers to add the user-agent."""
1756 if headers is None:
1757 headers = {}
1758 if method == 'PATCH':
1759 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001760 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001761 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1762 headers['x-http-method-override'] = "PATCH"
1763 method = 'POST'
1764 resp, content = request_orig(uri, method, body, headers,
1765 redirections, connection_type)
1766 return resp, content
1767
1768 http.request = new_request
1769 return http
Igor Maravić22435292017-01-19 22:28:22 +01001770
1771
1772def build_http():
1773 """Builds httplib2.Http object
1774
1775 Returns:
1776 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1777 To override default timeout call
1778
1779 socket.setdefaulttimeout(timeout_in_sec)
1780
1781 before interacting with this method.
1782 """
1783 if socket.getdefaulttimeout() is not None:
1784 http_timeout = socket.getdefaulttimeout()
1785 else:
1786 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1787 return httplib2.Http(timeout=http_timeout)