blob: 221b1c2b56b02e01edf27e47009f33e74eeb3c6e [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
19actuall HTTP request.
20"""
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
eesheeshc6425a02016-02-12 15:07:06 +000076_TOO_MANY_REQUESTS = 429
77
Igor Maravić22435292017-01-19 22:28:22 +010078DEFAULT_HTTP_TIMEOUT_SEC = 60
79
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070080_LEGACY_BATCH_URI = 'https://www.googleapis.com/batch'
81
eesheeshc6425a02016-02-12 15:07:06 +000082
83def _should_retry_response(resp_status, content):
84 """Determines whether a response should be retried.
85
86 Args:
87 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080088 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000089
90 Returns:
91 True if the response should be retried, otherwise False.
92 """
93 # Retry on 5xx errors.
94 if resp_status >= 500:
95 return True
96
97 # Retry on 429 errors.
98 if resp_status == _TOO_MANY_REQUESTS:
99 return True
100
101 # For 403 errors, we have to check for the `reason` in the response to
102 # determine if we should retry.
103 if resp_status == six.moves.http_client.FORBIDDEN:
104 # If there's no details about the 403 type, don't retry.
105 if not content:
106 return False
107
108 # Content is in JSON format.
109 try:
110 data = json.loads(content.decode('utf-8'))
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -0800111 if isinstance(data, dict):
112 reason = data['error']['errors'][0]['reason']
113 else:
114 reason = data[0]['error']['errors']['reason']
eesheeshc6425a02016-02-12 15:07:06 +0000115 except (UnicodeDecodeError, ValueError, KeyError):
116 LOGGER.warning('Invalid JSON content from response: %s', content)
117 return False
118
119 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
120
121 # Only retry on rate limit related failures.
122 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
123 return True
124
125 # Everything else is a success or non-retriable so break.
126 return False
127
John Asmuth864311d2014-04-24 15:46:08 -0400128
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100129def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
130 **kwargs):
131 """Retries an HTTP request multiple times while handling errors.
132
133 If after all retries the request still fails, last error is either returned as
134 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
135
136 Args:
137 http: Http object to be used to execute request.
138 num_retries: Maximum number of retries.
139 req_type: Type of the request (used for logging retries).
140 sleep, rand: Functions to sleep for random time between retries.
141 uri: URI to be requested.
142 method: HTTP method to be used.
143 args, kwargs: Additional arguments passed to http.request.
144
145 Returns:
146 resp, content - Response from the http request (may be HTTP 5xx).
147 """
148 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000149 content = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100150 for retry_num in range(num_retries + 1):
151 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000152 # Sleep before retrying.
153 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800154 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000155 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
156 sleep_time, retry_num, num_retries, req_type, method, uri,
157 resp.status if resp else exception)
158 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100159
160 try:
eesheeshc6425a02016-02-12 15:07:06 +0000161 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100162 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000163 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000164 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000165 exception = ssl_error
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700166 except socket.timeout as socket_timeout:
167 # It's important that this be before socket.error as it's a subclass
168 # socket.timeout has no errorcode
169 exception = socket_timeout
eesheeshc6425a02016-02-12 15:07:06 +0000170 except socket.error as socket_error:
171 # errno's contents differ by platform, so we have to match by name.
Alexander Mohrfff3ae52018-04-27 13:39:53 -0700172 if socket.errno.errorcode.get(socket_error.errno) not in {
173 'WSAETIMEDOUT', 'ETIMEDOUT', 'EPIPE', 'ECONNABORTED'}:
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100174 raise
eesheeshc6425a02016-02-12 15:07:06 +0000175 exception = socket_error
176
177 if exception:
178 if retry_num == num_retries:
179 raise exception
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100180 else:
181 continue
eesheeshc6425a02016-02-12 15:07:06 +0000182
183 if not _should_retry_response(resp.status, content):
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100184 break
185
186 return resp, content
187
188
John Asmuth864311d2014-04-24 15:46:08 -0400189class MediaUploadProgress(object):
190 """Status of a resumable upload."""
191
192 def __init__(self, resumable_progress, total_size):
193 """Constructor.
194
195 Args:
196 resumable_progress: int, bytes sent so far.
197 total_size: int, total bytes in complete upload, or None if the total
198 upload size isn't known ahead of time.
199 """
200 self.resumable_progress = resumable_progress
201 self.total_size = total_size
202
203 def progress(self):
204 """Percent of upload completed, as a float.
205
206 Returns:
207 the percentage complete as a float, returning 0.0 if the total size of
208 the upload is unknown.
209 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300210 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400211 return float(self.resumable_progress) / float(self.total_size)
212 else:
213 return 0.0
214
215
216class MediaDownloadProgress(object):
217 """Status of a resumable download."""
218
219 def __init__(self, resumable_progress, total_size):
220 """Constructor.
221
222 Args:
223 resumable_progress: int, bytes received so far.
224 total_size: int, total bytes in complete download.
225 """
226 self.resumable_progress = resumable_progress
227 self.total_size = total_size
228
229 def progress(self):
230 """Percent of download completed, as a float.
231
232 Returns:
233 the percentage complete as a float, returning 0.0 if the total size of
234 the download is unknown.
235 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300236 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400237 return float(self.resumable_progress) / float(self.total_size)
238 else:
239 return 0.0
240
241
242class MediaUpload(object):
243 """Describes a media object to upload.
244
245 Base class that defines the interface of MediaUpload subclasses.
246
247 Note that subclasses of MediaUpload may allow you to control the chunksize
248 when uploading a media object. It is important to keep the size of the chunk
249 as large as possible to keep the upload efficient. Other factors may influence
250 the size of the chunk you use, particularly if you are working in an
251 environment where individual HTTP requests may have a hardcoded time limit,
252 such as under certain classes of requests under Google App Engine.
253
254 Streams are io.Base compatible objects that support seek(). Some MediaUpload
255 subclasses support using streams directly to upload data. Support for
256 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
257 platform that stream will be used for uploading the media object. The support
258 for streaming is indicated by has_stream() returning True. The stream() method
259 should return an io.Base object that supports seek(). On platforms where the
260 underlying httplib module supports streaming, for example Python 2.6 and
261 later, the stream will be passed into the http library which will result in
262 less memory being used and possibly faster uploads.
263
264 If you need to upload media that can't be uploaded using any of the existing
265 MediaUpload sub-class then you can sub-class MediaUpload for your particular
266 needs.
267 """
268
269 def chunksize(self):
270 """Chunk size for resumable uploads.
271
272 Returns:
273 Chunk size in bytes.
274 """
275 raise NotImplementedError()
276
277 def mimetype(self):
278 """Mime type of the body.
279
280 Returns:
281 Mime type.
282 """
283 return 'application/octet-stream'
284
285 def size(self):
286 """Size of upload.
287
288 Returns:
289 Size of the body, or None of the size is unknown.
290 """
291 return None
292
293 def resumable(self):
294 """Whether this upload is resumable.
295
296 Returns:
297 True if resumable upload or False.
298 """
299 return False
300
301 def getbytes(self, begin, end):
302 """Get bytes from the media.
303
304 Args:
305 begin: int, offset from beginning of file.
306 length: int, number of bytes to read, starting at begin.
307
308 Returns:
309 A string of bytes read. May be shorter than length if EOF was reached
310 first.
311 """
312 raise NotImplementedError()
313
314 def has_stream(self):
315 """Does the underlying upload support a streaming interface.
316
317 Streaming means it is an io.IOBase subclass that supports seek, i.e.
318 seekable() returns True.
319
320 Returns:
321 True if the call to stream() will return an instance of a seekable io.Base
322 subclass.
323 """
324 return False
325
326 def stream(self):
327 """A stream interface to the data being uploaded.
328
329 Returns:
330 The returned value is an io.IOBase subclass that supports seek, i.e.
331 seekable() returns True.
332 """
333 raise NotImplementedError()
334
335 @util.positional(1)
336 def _to_json(self, strip=None):
337 """Utility function for creating a JSON representation of a MediaUpload.
338
339 Args:
340 strip: array, An array of names of members to not include in the JSON.
341
342 Returns:
343 string, a JSON representation of this instance, suitable to pass to
344 from_json().
345 """
346 t = type(self)
347 d = copy.copy(self.__dict__)
348 if strip is not None:
349 for member in strip:
350 del d[member]
351 d['_class'] = t.__name__
352 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700353 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400354
355 def to_json(self):
356 """Create a JSON representation of an instance of MediaUpload.
357
358 Returns:
359 string, a JSON representation of this instance, suitable to pass to
360 from_json().
361 """
362 return self._to_json()
363
364 @classmethod
365 def new_from_json(cls, s):
366 """Utility class method to instantiate a MediaUpload subclass from a JSON
367 representation produced by to_json().
368
369 Args:
370 s: string, JSON from to_json().
371
372 Returns:
373 An instance of the subclass of MediaUpload that was serialized with
374 to_json().
375 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700376 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400377 # Find and call the right classmethod from_json() to restore the object.
378 module = data['_module']
379 m = __import__(module, fromlist=module.split('.')[:-1])
380 kls = getattr(m, data['_class'])
381 from_json = getattr(kls, 'from_json')
382 return from_json(s)
383
384
385class MediaIoBaseUpload(MediaUpload):
386 """A MediaUpload for a io.Base objects.
387
388 Note that the Python file object is compatible with io.Base and can be used
389 with this class also.
390
Pat Ferateed9affd2015-03-03 16:03:15 -0800391 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400392 media = MediaIoBaseUpload(fh, mimetype='image/png',
393 chunksize=1024*1024, resumable=True)
394 farm.animals().insert(
395 id='cow',
396 name='cow.png',
397 media_body=media).execute()
398
399 Depending on the platform you are working on, you may pass -1 as the
400 chunksize, which indicates that the entire file should be uploaded in a single
401 request. If the underlying platform supports streams, such as Python 2.6 or
402 later, then this can be very efficient as it avoids multiple connections, and
403 also avoids loading the entire file into memory before sending it. Note that
404 Google App Engine has a 5MB limit on request size, so you should never set
405 your chunksize larger than 5MB, or to -1.
406 """
407
408 @util.positional(3)
409 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
410 resumable=False):
411 """Constructor.
412
413 Args:
414 fd: io.Base or file object, The source of the bytes to upload. MUST be
415 opened in blocking mode, do not use streams opened in non-blocking mode.
416 The given stream must be seekable, that is, it must be able to call
417 seek() on fd.
418 mimetype: string, Mime-type of the file.
419 chunksize: int, File will be uploaded in chunks of this many bytes. Only
420 used if resumable=True. Pass in a value of -1 if the file is to be
421 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
422 on request size, so you should never set your chunksize larger than 5MB,
423 or to -1.
424 resumable: bool, True if this is a resumable upload. False means upload
425 in a single request.
426 """
427 super(MediaIoBaseUpload, self).__init__()
428 self._fd = fd
429 self._mimetype = mimetype
430 if not (chunksize == -1 or chunksize > 0):
431 raise InvalidChunkSizeError()
432 self._chunksize = chunksize
433 self._resumable = resumable
434
435 self._fd.seek(0, os.SEEK_END)
436 self._size = self._fd.tell()
437
438 def chunksize(self):
439 """Chunk size for resumable uploads.
440
441 Returns:
442 Chunk size in bytes.
443 """
444 return self._chunksize
445
446 def mimetype(self):
447 """Mime type of the body.
448
449 Returns:
450 Mime type.
451 """
452 return self._mimetype
453
454 def size(self):
455 """Size of upload.
456
457 Returns:
458 Size of the body, or None of the size is unknown.
459 """
460 return self._size
461
462 def resumable(self):
463 """Whether this upload is resumable.
464
465 Returns:
466 True if resumable upload or False.
467 """
468 return self._resumable
469
470 def getbytes(self, begin, length):
471 """Get bytes from the media.
472
473 Args:
474 begin: int, offset from beginning of file.
475 length: int, number of bytes to read, starting at begin.
476
477 Returns:
478 A string of bytes read. May be shorted than length if EOF was reached
479 first.
480 """
481 self._fd.seek(begin)
482 return self._fd.read(length)
483
484 def has_stream(self):
485 """Does the underlying upload support a streaming interface.
486
487 Streaming means it is an io.IOBase subclass that supports seek, i.e.
488 seekable() returns True.
489
490 Returns:
491 True if the call to stream() will return an instance of a seekable io.Base
492 subclass.
493 """
494 return True
495
496 def stream(self):
497 """A stream interface to the data being uploaded.
498
499 Returns:
500 The returned value is an io.IOBase subclass that supports seek, i.e.
501 seekable() returns True.
502 """
503 return self._fd
504
505 def to_json(self):
506 """This upload type is not serializable."""
507 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
508
509
510class MediaFileUpload(MediaIoBaseUpload):
511 """A MediaUpload for a file.
512
513 Construct a MediaFileUpload and pass as the media_body parameter of the
514 method. For example, if we had a service that allowed uploading images:
515
John Asmuth864311d2014-04-24 15:46:08 -0400516 media = MediaFileUpload('cow.png', mimetype='image/png',
517 chunksize=1024*1024, resumable=True)
518 farm.animals().insert(
519 id='cow',
520 name='cow.png',
521 media_body=media).execute()
522
523 Depending on the platform you are working on, you may pass -1 as the
524 chunksize, which indicates that the entire file should be uploaded in a single
525 request. If the underlying platform supports streams, such as Python 2.6 or
526 later, then this can be very efficient as it avoids multiple connections, and
527 also avoids loading the entire file into memory before sending it. Note that
528 Google App Engine has a 5MB limit on request size, so you should never set
529 your chunksize larger than 5MB, or to -1.
530 """
531
532 @util.positional(2)
533 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
534 resumable=False):
535 """Constructor.
536
537 Args:
538 filename: string, Name of the file.
539 mimetype: string, Mime-type of the file. If None then a mime-type will be
540 guessed from the file extension.
541 chunksize: int, File will be uploaded in chunks of this many bytes. Only
542 used if resumable=True. Pass in a value of -1 if the file is to be
543 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
544 on request size, so you should never set your chunksize larger than 5MB,
545 or to -1.
546 resumable: bool, True if this is a resumable upload. False means upload
547 in a single request.
548 """
549 self._filename = filename
550 fd = open(self._filename, 'rb')
551 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800552 # No mimetype provided, make a guess.
553 mimetype, _ = mimetypes.guess_type(filename)
554 if mimetype is None:
555 # Guess failed, use octet-stream.
556 mimetype = 'application/octet-stream'
John Asmuth864311d2014-04-24 15:46:08 -0400557 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
558 resumable=resumable)
559
560 def to_json(self):
561 """Creating a JSON representation of an instance of MediaFileUpload.
562
563 Returns:
564 string, a JSON representation of this instance, suitable to pass to
565 from_json().
566 """
567 return self._to_json(strip=['_fd'])
568
569 @staticmethod
570 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700571 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400572 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
573 chunksize=d['_chunksize'], resumable=d['_resumable'])
574
575
576class MediaInMemoryUpload(MediaIoBaseUpload):
577 """MediaUpload for a chunk of bytes.
578
579 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
580 the stream.
581 """
582
583 @util.positional(2)
584 def __init__(self, body, mimetype='application/octet-stream',
585 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
586 """Create a new MediaInMemoryUpload.
587
588 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
589 the stream.
590
591 Args:
592 body: string, Bytes of body content.
593 mimetype: string, Mime-type of the file or default of
594 'application/octet-stream'.
595 chunksize: int, File will be uploaded in chunks of this many bytes. Only
596 used if resumable=True.
597 resumable: bool, True if this is a resumable upload. False means upload
598 in a single request.
599 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800600 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400601 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
602 resumable=resumable)
603
604
605class MediaIoBaseDownload(object):
606 """"Download media resources.
607
608 Note that the Python file object is compatible with io.Base and can be used
609 with this class also.
610
611
612 Example:
613 request = farms.animals().get_media(id='cow')
614 fh = io.FileIO('cow.png', mode='wb')
615 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
616
617 done = False
618 while done is False:
619 status, done = downloader.next_chunk()
620 if status:
621 print "Download %d%%." % int(status.progress() * 100)
622 print "Download Complete!"
623 """
624
625 @util.positional(3)
626 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
627 """Constructor.
628
629 Args:
630 fd: io.Base or file object, The stream in which to write the downloaded
631 bytes.
632 request: googleapiclient.http.HttpRequest, the media request to perform in
633 chunks.
634 chunksize: int, File will be downloaded in chunks of this many bytes.
635 """
636 self._fd = fd
637 self._request = request
638 self._uri = request.uri
639 self._chunksize = chunksize
640 self._progress = 0
641 self._total_size = None
642 self._done = False
643
644 # Stubs for testing.
645 self._sleep = time.sleep
646 self._rand = random.random
647
648 @util.positional(1)
649 def next_chunk(self, num_retries=0):
650 """Get the next chunk of the download.
651
652 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500653 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400654 exponential backoff. If all retries fail, the raised HttpError
655 represents the last request. If zero (default), we attempt the
656 request only once.
657
658 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700659 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400660 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100661 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400662
663 Raises:
664 googleapiclient.errors.HttpError if the response was not a 2xx.
665 httplib2.HttpLib2Error if a transport error has occured.
666 """
667 headers = {
668 'range': 'bytes=%d-%d' % (
669 self._progress, self._progress + self._chunksize)
670 }
671 http = self._request.http
672
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100673 resp, content = _retry_request(
674 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
675 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400676
677 if resp.status in [200, 206]:
678 if 'content-location' in resp and resp['content-location'] != self._uri:
679 self._uri = resp['content-location']
680 self._progress += len(content)
681 self._fd.write(content)
682
683 if 'content-range' in resp:
684 content_range = resp['content-range']
685 length = content_range.rsplit('/', 1)[1]
686 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800687 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800688 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400689
Daniel44067782018-01-16 23:17:56 +0100690 if self._total_size is None or self._progress == self._total_size:
John Asmuth864311d2014-04-24 15:46:08 -0400691 self._done = True
692 return MediaDownloadProgress(self._progress, self._total_size), self._done
693 else:
694 raise HttpError(resp, content, uri=self._uri)
695
696
697class _StreamSlice(object):
698 """Truncated stream.
699
700 Takes a stream and presents a stream that is a slice of the original stream.
701 This is used when uploading media in chunks. In later versions of Python a
702 stream can be passed to httplib in place of the string of data to send. The
703 problem is that httplib just blindly reads to the end of the stream. This
704 wrapper presents a virtual stream that only reads to the end of the chunk.
705 """
706
707 def __init__(self, stream, begin, chunksize):
708 """Constructor.
709
710 Args:
711 stream: (io.Base, file object), the stream to wrap.
712 begin: int, the seek position the chunk begins at.
713 chunksize: int, the size of the chunk.
714 """
715 self._stream = stream
716 self._begin = begin
717 self._chunksize = chunksize
718 self._stream.seek(begin)
719
720 def read(self, n=-1):
721 """Read n bytes.
722
723 Args:
724 n, int, the number of bytes to read.
725
726 Returns:
727 A string of length 'n', or less if EOF is reached.
728 """
729 # The data left available to read sits in [cur, end)
730 cur = self._stream.tell()
731 end = self._begin + self._chunksize
732 if n == -1 or cur + n > end:
733 n = end - cur
734 return self._stream.read(n)
735
736
737class HttpRequest(object):
738 """Encapsulates a single HTTP request."""
739
740 @util.positional(4)
741 def __init__(self, http, postproc, uri,
742 method='GET',
743 body=None,
744 headers=None,
745 methodId=None,
746 resumable=None):
747 """Constructor for an HttpRequest.
748
749 Args:
750 http: httplib2.Http, the transport object to use to make a request
751 postproc: callable, called on the HTTP response and content to transform
752 it into a data object before returning, or raising an exception
753 on an error.
754 uri: string, the absolute URI to send the request to
755 method: string, the HTTP method to use
756 body: string, the request body of the HTTP request,
757 headers: dict, the HTTP request headers
758 methodId: string, a unique identifier for the API method being called.
759 resumable: MediaUpload, None if this is not a resumbale request.
760 """
761 self.uri = uri
762 self.method = method
763 self.body = body
764 self.headers = headers or {}
765 self.methodId = methodId
766 self.http = http
767 self.postproc = postproc
768 self.resumable = resumable
769 self.response_callbacks = []
770 self._in_error_state = False
771
John Asmuth864311d2014-04-24 15:46:08 -0400772 # The size of the non-media part of the request.
773 self.body_size = len(self.body or '')
774
775 # The resumable URI to send chunks to.
776 self.resumable_uri = None
777
778 # The bytes that have been uploaded.
779 self.resumable_progress = 0
780
781 # Stubs for testing.
782 self._rand = random.random
783 self._sleep = time.sleep
784
785 @util.positional(1)
786 def execute(self, http=None, num_retries=0):
787 """Execute the request.
788
789 Args:
790 http: httplib2.Http, an http object to be used in place of the
791 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500792 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400793 exponential backoff. If all retries fail, the raised HttpError
794 represents the last request. If zero (default), we attempt the
795 request only once.
796
797 Returns:
798 A deserialized object model of the response body as determined
799 by the postproc.
800
801 Raises:
802 googleapiclient.errors.HttpError if the response was not a 2xx.
803 httplib2.HttpLib2Error if a transport error has occured.
804 """
805 if http is None:
806 http = self.http
807
808 if self.resumable:
809 body = None
810 while body is None:
811 _, body = self.next_chunk(http=http, num_retries=num_retries)
812 return body
813
814 # Non-resumable case.
815
816 if 'content-length' not in self.headers:
817 self.headers['content-length'] = str(self.body_size)
818 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800819 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400820 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
821 self.method = 'POST'
822 self.headers['x-http-method-override'] = 'GET'
823 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800824 parsed = urlparse(self.uri)
825 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400826 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
827 None)
828 )
829 self.body = parsed.query
830 self.headers['content-length'] = str(len(self.body))
831
832 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100833 resp, content = _retry_request(
834 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
835 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400836
837 for callback in self.response_callbacks:
838 callback(resp)
839 if resp.status >= 300:
840 raise HttpError(resp, content, uri=self.uri)
841 return self.postproc(resp, content)
842
843 @util.positional(2)
844 def add_response_callback(self, cb):
845 """add_response_headers_callback
846
847 Args:
848 cb: Callback to be called on receiving the response headers, of signature:
849
850 def cb(resp):
851 # Where resp is an instance of httplib2.Response
852 """
853 self.response_callbacks.append(cb)
854
855 @util.positional(1)
856 def next_chunk(self, http=None, num_retries=0):
857 """Execute the next step of a resumable upload.
858
859 Can only be used if the method being executed supports media uploads and
860 the MediaUpload object passed in was flagged as using resumable upload.
861
862 Example:
863
864 media = MediaFileUpload('cow.png', mimetype='image/png',
865 chunksize=1000, resumable=True)
866 request = farm.animals().insert(
867 id='cow',
868 name='cow.png',
869 media_body=media)
870
871 response = None
872 while response is None:
873 status, response = request.next_chunk()
874 if status:
875 print "Upload %d%% complete." % int(status.progress() * 100)
876
877
878 Args:
879 http: httplib2.Http, an http object to be used in place of the
880 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500881 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400882 exponential backoff. If all retries fail, the raised HttpError
883 represents the last request. If zero (default), we attempt the
884 request only once.
885
886 Returns:
887 (status, body): (ResumableMediaStatus, object)
888 The body will be None until the resumable media is fully uploaded.
889
890 Raises:
891 googleapiclient.errors.HttpError if the response was not a 2xx.
892 httplib2.HttpLib2Error if a transport error has occured.
893 """
894 if http is None:
895 http = self.http
896
897 if self.resumable.size() is None:
898 size = '*'
899 else:
900 size = str(self.resumable.size())
901
902 if self.resumable_uri is None:
903 start_headers = copy.copy(self.headers)
904 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
905 if size != '*':
906 start_headers['X-Upload-Content-Length'] = size
907 start_headers['content-length'] = str(self.body_size)
908
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100909 resp, content = _retry_request(
910 http, num_retries, 'resumable URI request', self._sleep, self._rand,
911 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400912
913 if resp.status == 200 and 'location' in resp:
914 self.resumable_uri = resp['location']
915 else:
916 raise ResumableUploadError(resp, content)
917 elif self._in_error_state:
918 # If we are in an error state then query the server for current state of
919 # the upload by sending an empty PUT and reading the 'range' header in
920 # the response.
921 headers = {
922 'Content-Range': 'bytes */%s' % size,
923 'content-length': '0'
924 }
925 resp, content = http.request(self.resumable_uri, 'PUT',
926 headers=headers)
927 status, body = self._process_response(resp, content)
928 if body:
929 # The upload was complete.
930 return (status, body)
931
e00Efafe8582015-10-10 18:19:37 +0200932 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400933 data = self.resumable.stream()
934 if self.resumable.chunksize() == -1:
935 data.seek(self.resumable_progress)
936 chunk_end = self.resumable.size() - self.resumable_progress - 1
937 else:
938 # Doing chunking with a stream, so wrap a slice of the stream.
939 data = _StreamSlice(data, self.resumable_progress,
940 self.resumable.chunksize())
941 chunk_end = min(
942 self.resumable_progress + self.resumable.chunksize() - 1,
943 self.resumable.size() - 1)
944 else:
945 data = self.resumable.getbytes(
946 self.resumable_progress, self.resumable.chunksize())
947
948 # A short read implies that we are at EOF, so finish the upload.
949 if len(data) < self.resumable.chunksize():
950 size = str(self.resumable_progress + len(data))
951
952 chunk_end = self.resumable_progress + len(data) - 1
953
954 headers = {
955 'Content-Range': 'bytes %d-%d/%s' % (
956 self.resumable_progress, chunk_end, size),
957 # Must set the content-length header here because httplib can't
958 # calculate the size when working with _StreamSlice.
959 'Content-Length': str(chunk_end - self.resumable_progress + 1)
960 }
961
INADA Naokie4ea1a92015-03-04 03:45:42 +0900962 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400963 if retry_num > 0:
964 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800965 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400966 'Retry #%d for media upload: %s %s, following status: %d'
967 % (retry_num, self.method, self.uri, resp.status))
968
969 try:
970 resp, content = http.request(self.resumable_uri, method='PUT',
971 body=data,
972 headers=headers)
973 except:
974 self._in_error_state = True
975 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500976 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400977 break
978
979 return self._process_response(resp, content)
980
981 def _process_response(self, resp, content):
982 """Process the response from a single chunk upload.
983
984 Args:
985 resp: httplib2.Response, the response object.
986 content: string, the content of the response.
987
988 Returns:
989 (status, body): (ResumableMediaStatus, object)
990 The body will be None until the resumable media is fully uploaded.
991
992 Raises:
993 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
994 """
995 if resp.status in [200, 201]:
996 self._in_error_state = False
997 return None, self.postproc(resp, content)
998 elif resp.status == 308:
999 self._in_error_state = False
1000 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001001 try:
1002 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1003 except KeyError:
1004 # If resp doesn't contain range header, resumable progress is 0
1005 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001006 if 'location' in resp:
1007 self.resumable_uri = resp['location']
1008 else:
1009 self._in_error_state = True
1010 raise HttpError(resp, content, uri=self.uri)
1011
1012 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1013 None)
1014
1015 def to_json(self):
1016 """Returns a JSON representation of the HttpRequest."""
1017 d = copy.copy(self.__dict__)
1018 if d['resumable'] is not None:
1019 d['resumable'] = self.resumable.to_json()
1020 del d['http']
1021 del d['postproc']
1022 del d['_sleep']
1023 del d['_rand']
1024
Craig Citro6ae34d72014-08-18 23:10:09 -07001025 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001026
1027 @staticmethod
1028 def from_json(s, http, postproc):
1029 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001030 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001031 if d['resumable'] is not None:
1032 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1033 return HttpRequest(
1034 http,
1035 postproc,
1036 uri=d['uri'],
1037 method=d['method'],
1038 body=d['body'],
1039 headers=d['headers'],
1040 methodId=d['methodId'],
1041 resumable=d['resumable'])
1042
1043
1044class BatchHttpRequest(object):
1045 """Batches multiple HttpRequest objects into a single HTTP request.
1046
1047 Example:
1048 from googleapiclient.http import BatchHttpRequest
1049
1050 def list_animals(request_id, response, exception):
1051 \"\"\"Do something with the animals list response.\"\"\"
1052 if exception is not None:
1053 # Do something with the exception.
1054 pass
1055 else:
1056 # Do something with the response.
1057 pass
1058
1059 def list_farmers(request_id, response, exception):
1060 \"\"\"Do something with the farmers list response.\"\"\"
1061 if exception is not None:
1062 # Do something with the exception.
1063 pass
1064 else:
1065 # Do something with the response.
1066 pass
1067
1068 service = build('farm', 'v2')
1069
1070 batch = BatchHttpRequest()
1071
1072 batch.add(service.animals().list(), list_animals)
1073 batch.add(service.farmers().list(), list_farmers)
1074 batch.execute(http=http)
1075 """
1076
1077 @util.positional(1)
1078 def __init__(self, callback=None, batch_uri=None):
1079 """Constructor for a BatchHttpRequest.
1080
1081 Args:
1082 callback: callable, A callback to be called for each response, of the
1083 form callback(id, response, exception). The first parameter is the
1084 request id, and the second is the deserialized response object. The
1085 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1086 occurred while processing the request, or None if no error occurred.
1087 batch_uri: string, URI to send batch requests to.
1088 """
1089 if batch_uri is None:
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001090 batch_uri = _LEGACY_BATCH_URI
1091
1092 if batch_uri == _LEGACY_BATCH_URI:
1093 LOGGER.warn(
1094 "You have constructed a BatchHttpRequest using the legacy batch "
1095 "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1096 "Please provide the API-specific endpoint or use "
1097 "service.new_batch_http_request(). For more details see "
1098 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1099 "and https://developers.google.com/api-client-library/python/guide/batch.",
1100 _LEGACY_BATCH_URI)
John Asmuth864311d2014-04-24 15:46:08 -04001101 self._batch_uri = batch_uri
1102
1103 # Global callback to be called for each individual response in the batch.
1104 self._callback = callback
1105
1106 # A map from id to request.
1107 self._requests = {}
1108
1109 # A map from id to callback.
1110 self._callbacks = {}
1111
1112 # List of request ids, in the order in which they were added.
1113 self._order = []
1114
1115 # The last auto generated id.
1116 self._last_auto_id = 0
1117
1118 # Unique ID on which to base the Content-ID headers.
1119 self._base_id = None
1120
1121 # A map from request id to (httplib2.Response, content) response pairs
1122 self._responses = {}
1123
1124 # A map of id(Credentials) that have been refreshed.
1125 self._refreshed_credentials = {}
1126
1127 def _refresh_and_apply_credentials(self, request, http):
1128 """Refresh the credentials and apply to the request.
1129
1130 Args:
1131 request: HttpRequest, the request.
1132 http: httplib2.Http, the global http object for the batch.
1133 """
1134 # For the credentials to refresh, but only once per refresh_token
1135 # If there is no http per the request then refresh the http passed in
1136 # via execute()
1137 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001138 request_credentials = False
1139
1140 if request.http is not None:
1141 creds = _auth.get_credentials_from_http(request.http)
1142 request_credentials = True
1143
1144 if creds is None and http is not None:
1145 creds = _auth.get_credentials_from_http(http)
1146
John Asmuth864311d2014-04-24 15:46:08 -04001147 if creds is not None:
1148 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001149 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001150 self._refreshed_credentials[id(creds)] = 1
1151
1152 # Only apply the credentials if we are using the http object passed in,
1153 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001154 if request.http is None or not request_credentials:
1155 _auth.apply_credentials(creds, request.headers)
1156
John Asmuth864311d2014-04-24 15:46:08 -04001157
1158 def _id_to_header(self, id_):
1159 """Convert an id to a Content-ID header value.
1160
1161 Args:
1162 id_: string, identifier of individual request.
1163
1164 Returns:
1165 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1166 the value because Content-ID headers are supposed to be universally
1167 unique.
1168 """
1169 if self._base_id is None:
1170 self._base_id = uuid.uuid4()
1171
Pat Ferated5b61bd2015-03-03 16:04:11 -08001172 return '<%s+%s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001173
1174 def _header_to_id(self, header):
1175 """Convert a Content-ID header value to an id.
1176
1177 Presumes the Content-ID header conforms to the format that _id_to_header()
1178 returns.
1179
1180 Args:
1181 header: string, Content-ID header value.
1182
1183 Returns:
1184 The extracted id value.
1185
1186 Raises:
1187 BatchError if the header is not in the expected format.
1188 """
1189 if header[0] != '<' or header[-1] != '>':
1190 raise BatchError("Invalid value for Content-ID: %s" % header)
1191 if '+' not in header:
1192 raise BatchError("Invalid value for Content-ID: %s" % header)
1193 base, id_ = header[1:-1].rsplit('+', 1)
1194
Pat Ferated5b61bd2015-03-03 16:04:11 -08001195 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001196
1197 def _serialize_request(self, request):
1198 """Convert an HttpRequest object into a string.
1199
1200 Args:
1201 request: HttpRequest, the request to serialize.
1202
1203 Returns:
1204 The request as a string in application/http format.
1205 """
1206 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001207 parsed = urlparse(request.uri)
1208 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001209 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001210 )
1211 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1212 major, minor = request.headers.get('content-type', 'application/json').split('/')
1213 msg = MIMENonMultipart(major, minor)
1214 headers = request.headers.copy()
1215
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001216 if request.http is not None:
1217 credentials = _auth.get_credentials_from_http(request.http)
1218 if credentials is not None:
1219 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001220
1221 # MIMENonMultipart adds its own Content-Type header.
1222 if 'content-type' in headers:
1223 del headers['content-type']
1224
INADA Naokie4ea1a92015-03-04 03:45:42 +09001225 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001226 msg[key] = value
1227 msg['Host'] = parsed.netloc
1228 msg.set_unixfrom(None)
1229
1230 if request.body is not None:
1231 msg.set_payload(request.body)
1232 msg['content-length'] = str(len(request.body))
1233
1234 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001235 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001236 # maxheaderlen=0 means don't line wrap headers.
1237 g = Generator(fp, maxheaderlen=0)
1238 g.flatten(msg, unixfrom=False)
1239 body = fp.getvalue()
1240
Pat Feratec9abbbd2015-03-03 18:00:38 -08001241 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001242
1243 def _deserialize_response(self, payload):
1244 """Convert string into httplib2 response and content.
1245
1246 Args:
1247 payload: string, headers and body as a string.
1248
1249 Returns:
1250 A pair (resp, content), such as would be returned from httplib2.request.
1251 """
1252 # Strip off the status line
1253 status_line, payload = payload.split('\n', 1)
1254 protocol, status, reason = status_line.split(' ', 2)
1255
1256 # Parse the rest of the response
1257 parser = FeedParser()
1258 parser.feed(payload)
1259 msg = parser.close()
1260 msg['status'] = status
1261
1262 # Create httplib2.Response from the parsed headers.
1263 resp = httplib2.Response(msg)
1264 resp.reason = reason
1265 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1266
1267 content = payload.split('\r\n\r\n', 1)[1]
1268
1269 return resp, content
1270
1271 def _new_id(self):
1272 """Create a new id.
1273
1274 Auto incrementing number that avoids conflicts with ids already used.
1275
1276 Returns:
1277 string, a new unique id.
1278 """
1279 self._last_auto_id += 1
1280 while str(self._last_auto_id) in self._requests:
1281 self._last_auto_id += 1
1282 return str(self._last_auto_id)
1283
1284 @util.positional(2)
1285 def add(self, request, callback=None, request_id=None):
1286 """Add a new request.
1287
1288 Every callback added will be paired with a unique id, the request_id. That
1289 unique id will be passed back to the callback when the response comes back
1290 from the server. The default behavior is to have the library generate it's
1291 own unique id. If the caller passes in a request_id then they must ensure
1292 uniqueness for each request_id, and if they are not an exception is
1293 raised. Callers should either supply all request_ids or nevery supply a
1294 request id, to avoid such an error.
1295
1296 Args:
1297 request: HttpRequest, Request to add to the batch.
1298 callback: callable, A callback to be called for this response, of the
1299 form callback(id, response, exception). The first parameter is the
1300 request id, and the second is the deserialized response object. The
1301 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1302 occurred while processing the request, or None if no errors occurred.
1303 request_id: string, A unique id for the request. The id will be passed to
1304 the callback with the response.
1305
1306 Returns:
1307 None
1308
1309 Raises:
1310 BatchError if a media request is added to a batch.
1311 KeyError is the request_id is not unique.
1312 """
1313 if request_id is None:
1314 request_id = self._new_id()
1315 if request.resumable is not None:
1316 raise BatchError("Media requests cannot be used in a batch request.")
1317 if request_id in self._requests:
1318 raise KeyError("A request with this ID already exists: %s" % request_id)
1319 self._requests[request_id] = request
1320 self._callbacks[request_id] = callback
1321 self._order.append(request_id)
1322
1323 def _execute(self, http, order, requests):
1324 """Serialize batch request, send to server, process response.
1325
1326 Args:
1327 http: httplib2.Http, an http object to be used to make the request with.
1328 order: list, list of request ids in the order they were added to the
1329 batch.
1330 request: list, list of request objects to send.
1331
1332 Raises:
1333 httplib2.HttpLib2Error if a transport error has occured.
1334 googleapiclient.errors.BatchError if the response is the wrong format.
1335 """
1336 message = MIMEMultipart('mixed')
1337 # Message should not write out it's own headers.
1338 setattr(message, '_write_headers', lambda self: None)
1339
1340 # Add all the individual requests.
1341 for request_id in order:
1342 request = requests[request_id]
1343
1344 msg = MIMENonMultipart('application', 'http')
1345 msg['Content-Transfer-Encoding'] = 'binary'
1346 msg['Content-ID'] = self._id_to_header(request_id)
1347
1348 body = self._serialize_request(request)
1349 msg.set_payload(body)
1350 message.attach(msg)
1351
Craig Citro72389b72014-07-15 17:12:50 -07001352 # encode the body: note that we can't use `as_string`, because
1353 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001354 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001355 g = Generator(fp, mangle_from_=False)
1356 g.flatten(message, unixfrom=False)
1357 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001358
1359 headers = {}
1360 headers['content-type'] = ('multipart/mixed; '
1361 'boundary="%s"') % message.get_boundary()
1362
1363 resp, content = http.request(self._batch_uri, method='POST', body=body,
1364 headers=headers)
1365
1366 if resp.status >= 300:
1367 raise HttpError(resp, content, uri=self._batch_uri)
1368
John Asmuth864311d2014-04-24 15:46:08 -04001369 # Prepend with a content-type header so FeedParser can handle it.
1370 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001371 # PY3's FeedParser only accepts unicode. So we should decode content
1372 # here, and encode each payload again.
1373 if six.PY3:
1374 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001375 for_parser = header + content
1376
1377 parser = FeedParser()
1378 parser.feed(for_parser)
1379 mime_response = parser.close()
1380
1381 if not mime_response.is_multipart():
1382 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1383 content=content)
1384
1385 for part in mime_response.get_payload():
1386 request_id = self._header_to_id(part['Content-ID'])
1387 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001388 # We encode content here to emulate normal http response.
1389 if isinstance(content, six.text_type):
1390 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001391 self._responses[request_id] = (response, content)
1392
1393 @util.positional(1)
1394 def execute(self, http=None):
1395 """Execute all the requests as a single batched HTTP request.
1396
1397 Args:
1398 http: httplib2.Http, an http object to be used in place of the one the
1399 HttpRequest request object was constructed with. If one isn't supplied
1400 then use a http object from the requests in this batch.
1401
1402 Returns:
1403 None
1404
1405 Raises:
1406 httplib2.HttpLib2Error if a transport error has occured.
1407 googleapiclient.errors.BatchError if the response is the wrong format.
1408 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001409 # If we have no requests return
1410 if len(self._order) == 0:
1411 return None
John Asmuth864311d2014-04-24 15:46:08 -04001412
1413 # If http is not supplied use the first valid one given in the requests.
1414 if http is None:
1415 for request_id in self._order:
1416 request = self._requests[request_id]
1417 if request is not None:
1418 http = request.http
1419 break
1420
1421 if http is None:
1422 raise ValueError("Missing a valid http object.")
1423
Gabriel Garcia23174be2016-05-25 17:28:07 +02001424 # Special case for OAuth2Credentials-style objects which have not yet been
1425 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001426 creds = _auth.get_credentials_from_http(http)
1427 if creds is not None:
1428 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001429 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001430 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001431
John Asmuth864311d2014-04-24 15:46:08 -04001432 self._execute(http, self._order, self._requests)
1433
1434 # Loop over all the requests and check for 401s. For each 401 request the
1435 # credentials should be refreshed and then sent again in a separate batch.
1436 redo_requests = {}
1437 redo_order = []
1438
1439 for request_id in self._order:
1440 resp, content = self._responses[request_id]
1441 if resp['status'] == '401':
1442 redo_order.append(request_id)
1443 request = self._requests[request_id]
1444 self._refresh_and_apply_credentials(request, http)
1445 redo_requests[request_id] = request
1446
1447 if redo_requests:
1448 self._execute(http, redo_order, redo_requests)
1449
1450 # Now process all callbacks that are erroring, and raise an exception for
1451 # ones that return a non-2xx response? Or add extra parameter to callback
1452 # that contains an HttpError?
1453
1454 for request_id in self._order:
1455 resp, content = self._responses[request_id]
1456
1457 request = self._requests[request_id]
1458 callback = self._callbacks[request_id]
1459
1460 response = None
1461 exception = None
1462 try:
1463 if resp.status >= 300:
1464 raise HttpError(resp, content, uri=request.uri)
1465 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001466 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001467 exception = e
1468
1469 if callback is not None:
1470 callback(request_id, response, exception)
1471 if self._callback is not None:
1472 self._callback(request_id, response, exception)
1473
1474
1475class HttpRequestMock(object):
1476 """Mock of HttpRequest.
1477
1478 Do not construct directly, instead use RequestMockBuilder.
1479 """
1480
1481 def __init__(self, resp, content, postproc):
1482 """Constructor for HttpRequestMock
1483
1484 Args:
1485 resp: httplib2.Response, the response to emulate coming from the request
1486 content: string, the response body
1487 postproc: callable, the post processing function usually supplied by
1488 the model class. See model.JsonModel.response() as an example.
1489 """
1490 self.resp = resp
1491 self.content = content
1492 self.postproc = postproc
1493 if resp is None:
1494 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1495 if 'reason' in self.resp:
1496 self.resp.reason = self.resp['reason']
1497
1498 def execute(self, http=None):
1499 """Execute the request.
1500
1501 Same behavior as HttpRequest.execute(), but the response is
1502 mocked and not really from an HTTP request/response.
1503 """
1504 return self.postproc(self.resp, self.content)
1505
1506
1507class RequestMockBuilder(object):
1508 """A simple mock of HttpRequest
1509
1510 Pass in a dictionary to the constructor that maps request methodIds to
1511 tuples of (httplib2.Response, content, opt_expected_body) that should be
1512 returned when that method is called. None may also be passed in for the
1513 httplib2.Response, in which case a 200 OK response will be generated.
1514 If an opt_expected_body (str or dict) is provided, it will be compared to
1515 the body and UnexpectedBodyError will be raised on inequality.
1516
1517 Example:
1518 response = '{"data": {"id": "tag:google.c...'
1519 requestBuilder = RequestMockBuilder(
1520 {
1521 'plus.activities.get': (None, response),
1522 }
1523 )
1524 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1525
1526 Methods that you do not supply a response for will return a
1527 200 OK with an empty string as the response content or raise an excpetion
1528 if check_unexpected is set to True. The methodId is taken from the rpcName
1529 in the discovery document.
1530
1531 For more details see the project wiki.
1532 """
1533
1534 def __init__(self, responses, check_unexpected=False):
1535 """Constructor for RequestMockBuilder
1536
1537 The constructed object should be a callable object
1538 that can replace the class HttpResponse.
1539
1540 responses - A dictionary that maps methodIds into tuples
1541 of (httplib2.Response, content). The methodId
1542 comes from the 'rpcName' field in the discovery
1543 document.
1544 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1545 should be raised on unsupplied method.
1546 """
1547 self.responses = responses
1548 self.check_unexpected = check_unexpected
1549
1550 def __call__(self, http, postproc, uri, method='GET', body=None,
1551 headers=None, methodId=None, resumable=None):
1552 """Implements the callable interface that discovery.build() expects
1553 of requestBuilder, which is to build an object compatible with
1554 HttpRequest.execute(). See that method for the description of the
1555 parameters and the expected response.
1556 """
1557 if methodId in self.responses:
1558 response = self.responses[methodId]
1559 resp, content = response[:2]
1560 if len(response) > 2:
1561 # Test the body against the supplied expected_body.
1562 expected_body = response[2]
1563 if bool(expected_body) != bool(body):
1564 # Not expecting a body and provided one
1565 # or expecting a body and not provided one.
1566 raise UnexpectedBodyError(expected_body, body)
1567 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001568 expected_body = json.loads(expected_body)
1569 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001570 if body != expected_body:
1571 raise UnexpectedBodyError(expected_body, body)
1572 return HttpRequestMock(resp, content, postproc)
1573 elif self.check_unexpected:
1574 raise UnexpectedMethodError(methodId=methodId)
1575 else:
1576 model = JsonModel(False)
1577 return HttpRequestMock(None, '{}', model.response)
1578
1579
1580class HttpMock(object):
1581 """Mock of httplib2.Http"""
1582
1583 def __init__(self, filename=None, headers=None):
1584 """
1585 Args:
1586 filename: string, absolute filename to read response from
1587 headers: dict, header to return with response
1588 """
1589 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001590 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001591 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001592 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001593 self.data = f.read()
1594 f.close()
1595 else:
1596 self.data = None
1597 self.response_headers = headers
1598 self.headers = None
1599 self.uri = None
1600 self.method = None
1601 self.body = None
1602 self.headers = None
1603
1604
1605 def request(self, uri,
1606 method='GET',
1607 body=None,
1608 headers=None,
1609 redirections=1,
1610 connection_type=None):
1611 self.uri = uri
1612 self.method = method
1613 self.body = body
1614 self.headers = headers
1615 return httplib2.Response(self.response_headers), self.data
1616
1617
1618class HttpMockSequence(object):
1619 """Mock of httplib2.Http
1620
1621 Mocks a sequence of calls to request returning different responses for each
1622 call. Create an instance initialized with the desired response headers
1623 and content and then use as if an httplib2.Http instance.
1624
1625 http = HttpMockSequence([
1626 ({'status': '401'}, ''),
1627 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1628 ({'status': '200'}, 'echo_request_headers'),
1629 ])
1630 resp, content = http.request("http://examples.com")
1631
1632 There are special values you can pass in for content to trigger
1633 behavours that are helpful in testing.
1634
1635 'echo_request_headers' means return the request headers in the response body
1636 'echo_request_headers_as_json' means return the request headers in
1637 the response body
1638 'echo_request_body' means return the request body in the response body
1639 'echo_request_uri' means return the request uri in the response body
1640 """
1641
1642 def __init__(self, iterable):
1643 """
1644 Args:
1645 iterable: iterable, a sequence of pairs of (headers, body)
1646 """
1647 self._iterable = iterable
1648 self.follow_redirects = True
1649
1650 def request(self, uri,
1651 method='GET',
1652 body=None,
1653 headers=None,
1654 redirections=1,
1655 connection_type=None):
1656 resp, content = self._iterable.pop(0)
1657 if content == 'echo_request_headers':
1658 content = headers
1659 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001660 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001661 elif content == 'echo_request_body':
1662 if hasattr(body, 'read'):
1663 content = body.read()
1664 else:
1665 content = body
1666 elif content == 'echo_request_uri':
1667 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001668 if isinstance(content, six.text_type):
1669 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001670 return httplib2.Response(resp), content
1671
1672
1673def set_user_agent(http, user_agent):
1674 """Set the user-agent on every request.
1675
1676 Args:
1677 http - An instance of httplib2.Http
1678 or something that acts like it.
1679 user_agent: string, the value for the user-agent header.
1680
1681 Returns:
1682 A modified instance of http that was passed in.
1683
1684 Example:
1685
1686 h = httplib2.Http()
1687 h = set_user_agent(h, "my-app-name/6.0")
1688
1689 Most of the time the user-agent will be set doing auth, this is for the rare
1690 cases where you are accessing an unauthenticated endpoint.
1691 """
1692 request_orig = http.request
1693
1694 # The closure that will replace 'httplib2.Http.request'.
1695 def new_request(uri, method='GET', body=None, headers=None,
1696 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1697 connection_type=None):
1698 """Modify the request headers to add the user-agent."""
1699 if headers is None:
1700 headers = {}
1701 if 'user-agent' in headers:
1702 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1703 else:
1704 headers['user-agent'] = user_agent
1705 resp, content = request_orig(uri, method, body, headers,
1706 redirections, connection_type)
1707 return resp, content
1708
1709 http.request = new_request
1710 return http
1711
1712
1713def tunnel_patch(http):
1714 """Tunnel PATCH requests over POST.
1715 Args:
1716 http - An instance of httplib2.Http
1717 or something that acts like it.
1718
1719 Returns:
1720 A modified instance of http that was passed in.
1721
1722 Example:
1723
1724 h = httplib2.Http()
1725 h = tunnel_patch(h, "my-app-name/6.0")
1726
1727 Useful if you are running on a platform that doesn't support PATCH.
1728 Apply this last if you are using OAuth 1.0, as changing the method
1729 will result in a different signature.
1730 """
1731 request_orig = http.request
1732
1733 # The closure that will replace 'httplib2.Http.request'.
1734 def new_request(uri, method='GET', body=None, headers=None,
1735 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1736 connection_type=None):
1737 """Modify the request headers to add the user-agent."""
1738 if headers is None:
1739 headers = {}
1740 if method == 'PATCH':
1741 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001742 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001743 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1744 headers['x-http-method-override'] = "PATCH"
1745 method = 'POST'
1746 resp, content = request_orig(uri, method, body, headers,
1747 redirections, connection_type)
1748 return resp, content
1749
1750 http.request = new_request
1751 return http
Igor Maravić22435292017-01-19 22:28:22 +01001752
1753
1754def build_http():
1755 """Builds httplib2.Http object
1756
1757 Returns:
1758 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1759 To override default timeout call
1760
1761 socket.setdefaulttimeout(timeout_in_sec)
1762
1763 before interacting with this method.
1764 """
1765 if socket.getdefaulttimeout() is not None:
1766 http_timeout = socket.getdefaulttimeout()
1767 else:
1768 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1769 return httplib2.Http(timeout=http_timeout)