blob: eb464490b17e799c1618729072d57c6d7aba803c [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
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070058# Oauth2client < 3 has the positional helper in 'util', >= 3 has it
59# in '_helpers'.
60try:
61 from oauth2client import util
62except ImportError:
63 from oauth2client import _helpers as util
64
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070065from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080066from googleapiclient import mimeparse
67from googleapiclient.errors import BatchError
68from googleapiclient.errors import HttpError
69from googleapiclient.errors import InvalidChunkSizeError
70from googleapiclient.errors import ResumableUploadError
71from googleapiclient.errors import UnexpectedBodyError
72from googleapiclient.errors import UnexpectedMethodError
73from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040074
75
Emmett Butler09699152016-02-08 14:26:00 -080076LOGGER = logging.getLogger(__name__)
77
John Asmuth864311d2014-04-24 15:46:08 -040078DEFAULT_CHUNK_SIZE = 512*1024
79
80MAX_URI_LENGTH = 2048
81
eesheeshc6425a02016-02-12 15:07:06 +000082_TOO_MANY_REQUESTS = 429
83
Igor Maravić22435292017-01-19 22:28:22 +010084DEFAULT_HTTP_TIMEOUT_SEC = 60
85
eesheeshc6425a02016-02-12 15:07:06 +000086
87def _should_retry_response(resp_status, content):
88 """Determines whether a response should be retried.
89
90 Args:
91 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080092 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000093
94 Returns:
95 True if the response should be retried, otherwise False.
96 """
97 # Retry on 5xx errors.
98 if resp_status >= 500:
99 return True
100
101 # Retry on 429 errors.
102 if resp_status == _TOO_MANY_REQUESTS:
103 return True
104
105 # For 403 errors, we have to check for the `reason` in the response to
106 # determine if we should retry.
107 if resp_status == six.moves.http_client.FORBIDDEN:
108 # If there's no details about the 403 type, don't retry.
109 if not content:
110 return False
111
112 # Content is in JSON format.
113 try:
114 data = json.loads(content.decode('utf-8'))
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -0800115 if isinstance(data, dict):
116 reason = data['error']['errors'][0]['reason']
117 else:
118 reason = data[0]['error']['errors']['reason']
eesheeshc6425a02016-02-12 15:07:06 +0000119 except (UnicodeDecodeError, ValueError, KeyError):
120 LOGGER.warning('Invalid JSON content from response: %s', content)
121 return False
122
123 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
124
125 # Only retry on rate limit related failures.
126 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
127 return True
128
129 # Everything else is a success or non-retriable so break.
130 return False
131
John Asmuth864311d2014-04-24 15:46:08 -0400132
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100133def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
134 **kwargs):
135 """Retries an HTTP request multiple times while handling errors.
136
137 If after all retries the request still fails, last error is either returned as
138 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
139
140 Args:
141 http: Http object to be used to execute request.
142 num_retries: Maximum number of retries.
143 req_type: Type of the request (used for logging retries).
144 sleep, rand: Functions to sleep for random time between retries.
145 uri: URI to be requested.
146 method: HTTP method to be used.
147 args, kwargs: Additional arguments passed to http.request.
148
149 Returns:
150 resp, content - Response from the http request (may be HTTP 5xx).
151 """
152 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000153 content = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100154 for retry_num in range(num_retries + 1):
155 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000156 # Sleep before retrying.
157 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800158 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000159 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
160 sleep_time, retry_num, num_retries, req_type, method, uri,
161 resp.status if resp else exception)
162 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100163
164 try:
eesheeshc6425a02016-02-12 15:07:06 +0000165 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100166 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000167 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000168 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000169 exception = ssl_error
170 except socket.error as socket_error:
171 # errno's contents differ by platform, so we have to match by name.
172 if socket.errno.errorcode.get(socket_error.errno) not in (
Thomas Bonfort88ab76b2016-04-19 08:48:53 +0200173 '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
661 downloaded.
662
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
690 if self._progress == self._total_size:
691 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
772 # Pull the multipart boundary out of the content-type header.
773 major, minor, params = mimeparse.parse_mime_type(
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100774 self.headers.get('content-type', 'application/json'))
John Asmuth864311d2014-04-24 15:46:08 -0400775
776 # The size of the non-media part of the request.
777 self.body_size = len(self.body or '')
778
779 # The resumable URI to send chunks to.
780 self.resumable_uri = None
781
782 # The bytes that have been uploaded.
783 self.resumable_progress = 0
784
785 # Stubs for testing.
786 self._rand = random.random
787 self._sleep = time.sleep
788
789 @util.positional(1)
790 def execute(self, http=None, num_retries=0):
791 """Execute the request.
792
793 Args:
794 http: httplib2.Http, an http object to be used in place of the
795 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500796 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400797 exponential backoff. If all retries fail, the raised HttpError
798 represents the last request. If zero (default), we attempt the
799 request only once.
800
801 Returns:
802 A deserialized object model of the response body as determined
803 by the postproc.
804
805 Raises:
806 googleapiclient.errors.HttpError if the response was not a 2xx.
807 httplib2.HttpLib2Error if a transport error has occured.
808 """
809 if http is None:
810 http = self.http
811
812 if self.resumable:
813 body = None
814 while body is None:
815 _, body = self.next_chunk(http=http, num_retries=num_retries)
816 return body
817
818 # Non-resumable case.
819
820 if 'content-length' not in self.headers:
821 self.headers['content-length'] = str(self.body_size)
822 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800823 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400824 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
825 self.method = 'POST'
826 self.headers['x-http-method-override'] = 'GET'
827 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800828 parsed = urlparse(self.uri)
829 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400830 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
831 None)
832 )
833 self.body = parsed.query
834 self.headers['content-length'] = str(len(self.body))
835
836 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100837 resp, content = _retry_request(
838 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
839 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400840
841 for callback in self.response_callbacks:
842 callback(resp)
843 if resp.status >= 300:
844 raise HttpError(resp, content, uri=self.uri)
845 return self.postproc(resp, content)
846
847 @util.positional(2)
848 def add_response_callback(self, cb):
849 """add_response_headers_callback
850
851 Args:
852 cb: Callback to be called on receiving the response headers, of signature:
853
854 def cb(resp):
855 # Where resp is an instance of httplib2.Response
856 """
857 self.response_callbacks.append(cb)
858
859 @util.positional(1)
860 def next_chunk(self, http=None, num_retries=0):
861 """Execute the next step of a resumable upload.
862
863 Can only be used if the method being executed supports media uploads and
864 the MediaUpload object passed in was flagged as using resumable upload.
865
866 Example:
867
868 media = MediaFileUpload('cow.png', mimetype='image/png',
869 chunksize=1000, resumable=True)
870 request = farm.animals().insert(
871 id='cow',
872 name='cow.png',
873 media_body=media)
874
875 response = None
876 while response is None:
877 status, response = request.next_chunk()
878 if status:
879 print "Upload %d%% complete." % int(status.progress() * 100)
880
881
882 Args:
883 http: httplib2.Http, an http object to be used in place of the
884 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500885 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400886 exponential backoff. If all retries fail, the raised HttpError
887 represents the last request. If zero (default), we attempt the
888 request only once.
889
890 Returns:
891 (status, body): (ResumableMediaStatus, object)
892 The body will be None until the resumable media is fully uploaded.
893
894 Raises:
895 googleapiclient.errors.HttpError if the response was not a 2xx.
896 httplib2.HttpLib2Error if a transport error has occured.
897 """
898 if http is None:
899 http = self.http
900
901 if self.resumable.size() is None:
902 size = '*'
903 else:
904 size = str(self.resumable.size())
905
906 if self.resumable_uri is None:
907 start_headers = copy.copy(self.headers)
908 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
909 if size != '*':
910 start_headers['X-Upload-Content-Length'] = size
911 start_headers['content-length'] = str(self.body_size)
912
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100913 resp, content = _retry_request(
914 http, num_retries, 'resumable URI request', self._sleep, self._rand,
915 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400916
917 if resp.status == 200 and 'location' in resp:
918 self.resumable_uri = resp['location']
919 else:
920 raise ResumableUploadError(resp, content)
921 elif self._in_error_state:
922 # If we are in an error state then query the server for current state of
923 # the upload by sending an empty PUT and reading the 'range' header in
924 # the response.
925 headers = {
926 'Content-Range': 'bytes */%s' % size,
927 'content-length': '0'
928 }
929 resp, content = http.request(self.resumable_uri, 'PUT',
930 headers=headers)
931 status, body = self._process_response(resp, content)
932 if body:
933 # The upload was complete.
934 return (status, body)
935
e00Efafe8582015-10-10 18:19:37 +0200936 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400937 data = self.resumable.stream()
938 if self.resumable.chunksize() == -1:
939 data.seek(self.resumable_progress)
940 chunk_end = self.resumable.size() - self.resumable_progress - 1
941 else:
942 # Doing chunking with a stream, so wrap a slice of the stream.
943 data = _StreamSlice(data, self.resumable_progress,
944 self.resumable.chunksize())
945 chunk_end = min(
946 self.resumable_progress + self.resumable.chunksize() - 1,
947 self.resumable.size() - 1)
948 else:
949 data = self.resumable.getbytes(
950 self.resumable_progress, self.resumable.chunksize())
951
952 # A short read implies that we are at EOF, so finish the upload.
953 if len(data) < self.resumable.chunksize():
954 size = str(self.resumable_progress + len(data))
955
956 chunk_end = self.resumable_progress + len(data) - 1
957
958 headers = {
959 'Content-Range': 'bytes %d-%d/%s' % (
960 self.resumable_progress, chunk_end, size),
961 # Must set the content-length header here because httplib can't
962 # calculate the size when working with _StreamSlice.
963 'Content-Length': str(chunk_end - self.resumable_progress + 1)
964 }
965
INADA Naokie4ea1a92015-03-04 03:45:42 +0900966 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400967 if retry_num > 0:
968 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800969 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400970 'Retry #%d for media upload: %s %s, following status: %d'
971 % (retry_num, self.method, self.uri, resp.status))
972
973 try:
974 resp, content = http.request(self.resumable_uri, method='PUT',
975 body=data,
976 headers=headers)
977 except:
978 self._in_error_state = True
979 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500980 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400981 break
982
983 return self._process_response(resp, content)
984
985 def _process_response(self, resp, content):
986 """Process the response from a single chunk upload.
987
988 Args:
989 resp: httplib2.Response, the response object.
990 content: string, the content of the response.
991
992 Returns:
993 (status, body): (ResumableMediaStatus, object)
994 The body will be None until the resumable media is fully uploaded.
995
996 Raises:
997 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
998 """
999 if resp.status in [200, 201]:
1000 self._in_error_state = False
1001 return None, self.postproc(resp, content)
1002 elif resp.status == 308:
1003 self._in_error_state = False
1004 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -08001005 try:
1006 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
1007 except KeyError:
1008 # If resp doesn't contain range header, resumable progress is 0
1009 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001010 if 'location' in resp:
1011 self.resumable_uri = resp['location']
1012 else:
1013 self._in_error_state = True
1014 raise HttpError(resp, content, uri=self.uri)
1015
1016 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1017 None)
1018
1019 def to_json(self):
1020 """Returns a JSON representation of the HttpRequest."""
1021 d = copy.copy(self.__dict__)
1022 if d['resumable'] is not None:
1023 d['resumable'] = self.resumable.to_json()
1024 del d['http']
1025 del d['postproc']
1026 del d['_sleep']
1027 del d['_rand']
1028
Craig Citro6ae34d72014-08-18 23:10:09 -07001029 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001030
1031 @staticmethod
1032 def from_json(s, http, postproc):
1033 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001034 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001035 if d['resumable'] is not None:
1036 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1037 return HttpRequest(
1038 http,
1039 postproc,
1040 uri=d['uri'],
1041 method=d['method'],
1042 body=d['body'],
1043 headers=d['headers'],
1044 methodId=d['methodId'],
1045 resumable=d['resumable'])
1046
1047
1048class BatchHttpRequest(object):
1049 """Batches multiple HttpRequest objects into a single HTTP request.
1050
1051 Example:
1052 from googleapiclient.http import BatchHttpRequest
1053
1054 def list_animals(request_id, response, exception):
1055 \"\"\"Do something with the animals list response.\"\"\"
1056 if exception is not None:
1057 # Do something with the exception.
1058 pass
1059 else:
1060 # Do something with the response.
1061 pass
1062
1063 def list_farmers(request_id, response, exception):
1064 \"\"\"Do something with the farmers list response.\"\"\"
1065 if exception is not None:
1066 # Do something with the exception.
1067 pass
1068 else:
1069 # Do something with the response.
1070 pass
1071
1072 service = build('farm', 'v2')
1073
1074 batch = BatchHttpRequest()
1075
1076 batch.add(service.animals().list(), list_animals)
1077 batch.add(service.farmers().list(), list_farmers)
1078 batch.execute(http=http)
1079 """
1080
1081 @util.positional(1)
1082 def __init__(self, callback=None, batch_uri=None):
1083 """Constructor for a BatchHttpRequest.
1084
1085 Args:
1086 callback: callable, A callback to be called for each response, of the
1087 form callback(id, response, exception). The first parameter is the
1088 request id, and the second is the deserialized response object. The
1089 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1090 occurred while processing the request, or None if no error occurred.
1091 batch_uri: string, URI to send batch requests to.
1092 """
1093 if batch_uri is None:
1094 batch_uri = 'https://www.googleapis.com/batch'
1095 self._batch_uri = batch_uri
1096
1097 # Global callback to be called for each individual response in the batch.
1098 self._callback = callback
1099
1100 # A map from id to request.
1101 self._requests = {}
1102
1103 # A map from id to callback.
1104 self._callbacks = {}
1105
1106 # List of request ids, in the order in which they were added.
1107 self._order = []
1108
1109 # The last auto generated id.
1110 self._last_auto_id = 0
1111
1112 # Unique ID on which to base the Content-ID headers.
1113 self._base_id = None
1114
1115 # A map from request id to (httplib2.Response, content) response pairs
1116 self._responses = {}
1117
1118 # A map of id(Credentials) that have been refreshed.
1119 self._refreshed_credentials = {}
1120
1121 def _refresh_and_apply_credentials(self, request, http):
1122 """Refresh the credentials and apply to the request.
1123
1124 Args:
1125 request: HttpRequest, the request.
1126 http: httplib2.Http, the global http object for the batch.
1127 """
1128 # For the credentials to refresh, but only once per refresh_token
1129 # If there is no http per the request then refresh the http passed in
1130 # via execute()
1131 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001132 request_credentials = False
1133
1134 if request.http is not None:
1135 creds = _auth.get_credentials_from_http(request.http)
1136 request_credentials = True
1137
1138 if creds is None and http is not None:
1139 creds = _auth.get_credentials_from_http(http)
1140
John Asmuth864311d2014-04-24 15:46:08 -04001141 if creds is not None:
1142 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001143 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001144 self._refreshed_credentials[id(creds)] = 1
1145
1146 # Only apply the credentials if we are using the http object passed in,
1147 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001148 if request.http is None or not request_credentials:
1149 _auth.apply_credentials(creds, request.headers)
1150
John Asmuth864311d2014-04-24 15:46:08 -04001151
1152 def _id_to_header(self, id_):
1153 """Convert an id to a Content-ID header value.
1154
1155 Args:
1156 id_: string, identifier of individual request.
1157
1158 Returns:
1159 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1160 the value because Content-ID headers are supposed to be universally
1161 unique.
1162 """
1163 if self._base_id is None:
1164 self._base_id = uuid.uuid4()
1165
Pat Ferated5b61bd2015-03-03 16:04:11 -08001166 return '<%s+%s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001167
1168 def _header_to_id(self, header):
1169 """Convert a Content-ID header value to an id.
1170
1171 Presumes the Content-ID header conforms to the format that _id_to_header()
1172 returns.
1173
1174 Args:
1175 header: string, Content-ID header value.
1176
1177 Returns:
1178 The extracted id value.
1179
1180 Raises:
1181 BatchError if the header is not in the expected format.
1182 """
1183 if header[0] != '<' or header[-1] != '>':
1184 raise BatchError("Invalid value for Content-ID: %s" % header)
1185 if '+' not in header:
1186 raise BatchError("Invalid value for Content-ID: %s" % header)
1187 base, id_ = header[1:-1].rsplit('+', 1)
1188
Pat Ferated5b61bd2015-03-03 16:04:11 -08001189 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001190
1191 def _serialize_request(self, request):
1192 """Convert an HttpRequest object into a string.
1193
1194 Args:
1195 request: HttpRequest, the request to serialize.
1196
1197 Returns:
1198 The request as a string in application/http format.
1199 """
1200 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001201 parsed = urlparse(request.uri)
1202 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001203 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001204 )
1205 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1206 major, minor = request.headers.get('content-type', 'application/json').split('/')
1207 msg = MIMENonMultipart(major, minor)
1208 headers = request.headers.copy()
1209
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001210 if request.http is not None:
1211 credentials = _auth.get_credentials_from_http(request.http)
1212 if credentials is not None:
1213 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001214
1215 # MIMENonMultipart adds its own Content-Type header.
1216 if 'content-type' in headers:
1217 del headers['content-type']
1218
INADA Naokie4ea1a92015-03-04 03:45:42 +09001219 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001220 msg[key] = value
1221 msg['Host'] = parsed.netloc
1222 msg.set_unixfrom(None)
1223
1224 if request.body is not None:
1225 msg.set_payload(request.body)
1226 msg['content-length'] = str(len(request.body))
1227
1228 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001229 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001230 # maxheaderlen=0 means don't line wrap headers.
1231 g = Generator(fp, maxheaderlen=0)
1232 g.flatten(msg, unixfrom=False)
1233 body = fp.getvalue()
1234
Pat Feratec9abbbd2015-03-03 18:00:38 -08001235 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001236
1237 def _deserialize_response(self, payload):
1238 """Convert string into httplib2 response and content.
1239
1240 Args:
1241 payload: string, headers and body as a string.
1242
1243 Returns:
1244 A pair (resp, content), such as would be returned from httplib2.request.
1245 """
1246 # Strip off the status line
1247 status_line, payload = payload.split('\n', 1)
1248 protocol, status, reason = status_line.split(' ', 2)
1249
1250 # Parse the rest of the response
1251 parser = FeedParser()
1252 parser.feed(payload)
1253 msg = parser.close()
1254 msg['status'] = status
1255
1256 # Create httplib2.Response from the parsed headers.
1257 resp = httplib2.Response(msg)
1258 resp.reason = reason
1259 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1260
1261 content = payload.split('\r\n\r\n', 1)[1]
1262
1263 return resp, content
1264
1265 def _new_id(self):
1266 """Create a new id.
1267
1268 Auto incrementing number that avoids conflicts with ids already used.
1269
1270 Returns:
1271 string, a new unique id.
1272 """
1273 self._last_auto_id += 1
1274 while str(self._last_auto_id) in self._requests:
1275 self._last_auto_id += 1
1276 return str(self._last_auto_id)
1277
1278 @util.positional(2)
1279 def add(self, request, callback=None, request_id=None):
1280 """Add a new request.
1281
1282 Every callback added will be paired with a unique id, the request_id. That
1283 unique id will be passed back to the callback when the response comes back
1284 from the server. The default behavior is to have the library generate it's
1285 own unique id. If the caller passes in a request_id then they must ensure
1286 uniqueness for each request_id, and if they are not an exception is
1287 raised. Callers should either supply all request_ids or nevery supply a
1288 request id, to avoid such an error.
1289
1290 Args:
1291 request: HttpRequest, Request to add to the batch.
1292 callback: callable, A callback to be called for this response, of the
1293 form callback(id, response, exception). The first parameter is the
1294 request id, and the second is the deserialized response object. The
1295 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1296 occurred while processing the request, or None if no errors occurred.
1297 request_id: string, A unique id for the request. The id will be passed to
1298 the callback with the response.
1299
1300 Returns:
1301 None
1302
1303 Raises:
1304 BatchError if a media request is added to a batch.
1305 KeyError is the request_id is not unique.
1306 """
1307 if request_id is None:
1308 request_id = self._new_id()
1309 if request.resumable is not None:
1310 raise BatchError("Media requests cannot be used in a batch request.")
1311 if request_id in self._requests:
1312 raise KeyError("A request with this ID already exists: %s" % request_id)
1313 self._requests[request_id] = request
1314 self._callbacks[request_id] = callback
1315 self._order.append(request_id)
1316
1317 def _execute(self, http, order, requests):
1318 """Serialize batch request, send to server, process response.
1319
1320 Args:
1321 http: httplib2.Http, an http object to be used to make the request with.
1322 order: list, list of request ids in the order they were added to the
1323 batch.
1324 request: list, list of request objects to send.
1325
1326 Raises:
1327 httplib2.HttpLib2Error if a transport error has occured.
1328 googleapiclient.errors.BatchError if the response is the wrong format.
1329 """
1330 message = MIMEMultipart('mixed')
1331 # Message should not write out it's own headers.
1332 setattr(message, '_write_headers', lambda self: None)
1333
1334 # Add all the individual requests.
1335 for request_id in order:
1336 request = requests[request_id]
1337
1338 msg = MIMENonMultipart('application', 'http')
1339 msg['Content-Transfer-Encoding'] = 'binary'
1340 msg['Content-ID'] = self._id_to_header(request_id)
1341
1342 body = self._serialize_request(request)
1343 msg.set_payload(body)
1344 message.attach(msg)
1345
Craig Citro72389b72014-07-15 17:12:50 -07001346 # encode the body: note that we can't use `as_string`, because
1347 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001348 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001349 g = Generator(fp, mangle_from_=False)
1350 g.flatten(message, unixfrom=False)
1351 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001352
1353 headers = {}
1354 headers['content-type'] = ('multipart/mixed; '
1355 'boundary="%s"') % message.get_boundary()
1356
1357 resp, content = http.request(self._batch_uri, method='POST', body=body,
1358 headers=headers)
1359
1360 if resp.status >= 300:
1361 raise HttpError(resp, content, uri=self._batch_uri)
1362
John Asmuth864311d2014-04-24 15:46:08 -04001363 # Prepend with a content-type header so FeedParser can handle it.
1364 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001365 # PY3's FeedParser only accepts unicode. So we should decode content
1366 # here, and encode each payload again.
1367 if six.PY3:
1368 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001369 for_parser = header + content
1370
1371 parser = FeedParser()
1372 parser.feed(for_parser)
1373 mime_response = parser.close()
1374
1375 if not mime_response.is_multipart():
1376 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1377 content=content)
1378
1379 for part in mime_response.get_payload():
1380 request_id = self._header_to_id(part['Content-ID'])
1381 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001382 # We encode content here to emulate normal http response.
1383 if isinstance(content, six.text_type):
1384 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001385 self._responses[request_id] = (response, content)
1386
1387 @util.positional(1)
1388 def execute(self, http=None):
1389 """Execute all the requests as a single batched HTTP request.
1390
1391 Args:
1392 http: httplib2.Http, an http object to be used in place of the one the
1393 HttpRequest request object was constructed with. If one isn't supplied
1394 then use a http object from the requests in this batch.
1395
1396 Returns:
1397 None
1398
1399 Raises:
1400 httplib2.HttpLib2Error if a transport error has occured.
1401 googleapiclient.errors.BatchError if the response is the wrong format.
1402 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001403 # If we have no requests return
1404 if len(self._order) == 0:
1405 return None
John Asmuth864311d2014-04-24 15:46:08 -04001406
1407 # If http is not supplied use the first valid one given in the requests.
1408 if http is None:
1409 for request_id in self._order:
1410 request = self._requests[request_id]
1411 if request is not None:
1412 http = request.http
1413 break
1414
1415 if http is None:
1416 raise ValueError("Missing a valid http object.")
1417
Gabriel Garcia23174be2016-05-25 17:28:07 +02001418 # Special case for OAuth2Credentials-style objects which have not yet been
1419 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001420 creds = _auth.get_credentials_from_http(http)
1421 if creds is not None:
1422 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001423 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001424 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001425
John Asmuth864311d2014-04-24 15:46:08 -04001426 self._execute(http, self._order, self._requests)
1427
1428 # Loop over all the requests and check for 401s. For each 401 request the
1429 # credentials should be refreshed and then sent again in a separate batch.
1430 redo_requests = {}
1431 redo_order = []
1432
1433 for request_id in self._order:
1434 resp, content = self._responses[request_id]
1435 if resp['status'] == '401':
1436 redo_order.append(request_id)
1437 request = self._requests[request_id]
1438 self._refresh_and_apply_credentials(request, http)
1439 redo_requests[request_id] = request
1440
1441 if redo_requests:
1442 self._execute(http, redo_order, redo_requests)
1443
1444 # Now process all callbacks that are erroring, and raise an exception for
1445 # ones that return a non-2xx response? Or add extra parameter to callback
1446 # that contains an HttpError?
1447
1448 for request_id in self._order:
1449 resp, content = self._responses[request_id]
1450
1451 request = self._requests[request_id]
1452 callback = self._callbacks[request_id]
1453
1454 response = None
1455 exception = None
1456 try:
1457 if resp.status >= 300:
1458 raise HttpError(resp, content, uri=request.uri)
1459 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001460 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001461 exception = e
1462
1463 if callback is not None:
1464 callback(request_id, response, exception)
1465 if self._callback is not None:
1466 self._callback(request_id, response, exception)
1467
1468
1469class HttpRequestMock(object):
1470 """Mock of HttpRequest.
1471
1472 Do not construct directly, instead use RequestMockBuilder.
1473 """
1474
1475 def __init__(self, resp, content, postproc):
1476 """Constructor for HttpRequestMock
1477
1478 Args:
1479 resp: httplib2.Response, the response to emulate coming from the request
1480 content: string, the response body
1481 postproc: callable, the post processing function usually supplied by
1482 the model class. See model.JsonModel.response() as an example.
1483 """
1484 self.resp = resp
1485 self.content = content
1486 self.postproc = postproc
1487 if resp is None:
1488 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1489 if 'reason' in self.resp:
1490 self.resp.reason = self.resp['reason']
1491
1492 def execute(self, http=None):
1493 """Execute the request.
1494
1495 Same behavior as HttpRequest.execute(), but the response is
1496 mocked and not really from an HTTP request/response.
1497 """
1498 return self.postproc(self.resp, self.content)
1499
1500
1501class RequestMockBuilder(object):
1502 """A simple mock of HttpRequest
1503
1504 Pass in a dictionary to the constructor that maps request methodIds to
1505 tuples of (httplib2.Response, content, opt_expected_body) that should be
1506 returned when that method is called. None may also be passed in for the
1507 httplib2.Response, in which case a 200 OK response will be generated.
1508 If an opt_expected_body (str or dict) is provided, it will be compared to
1509 the body and UnexpectedBodyError will be raised on inequality.
1510
1511 Example:
1512 response = '{"data": {"id": "tag:google.c...'
1513 requestBuilder = RequestMockBuilder(
1514 {
1515 'plus.activities.get': (None, response),
1516 }
1517 )
1518 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1519
1520 Methods that you do not supply a response for will return a
1521 200 OK with an empty string as the response content or raise an excpetion
1522 if check_unexpected is set to True. The methodId is taken from the rpcName
1523 in the discovery document.
1524
1525 For more details see the project wiki.
1526 """
1527
1528 def __init__(self, responses, check_unexpected=False):
1529 """Constructor for RequestMockBuilder
1530
1531 The constructed object should be a callable object
1532 that can replace the class HttpResponse.
1533
1534 responses - A dictionary that maps methodIds into tuples
1535 of (httplib2.Response, content). The methodId
1536 comes from the 'rpcName' field in the discovery
1537 document.
1538 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1539 should be raised on unsupplied method.
1540 """
1541 self.responses = responses
1542 self.check_unexpected = check_unexpected
1543
1544 def __call__(self, http, postproc, uri, method='GET', body=None,
1545 headers=None, methodId=None, resumable=None):
1546 """Implements the callable interface that discovery.build() expects
1547 of requestBuilder, which is to build an object compatible with
1548 HttpRequest.execute(). See that method for the description of the
1549 parameters and the expected response.
1550 """
1551 if methodId in self.responses:
1552 response = self.responses[methodId]
1553 resp, content = response[:2]
1554 if len(response) > 2:
1555 # Test the body against the supplied expected_body.
1556 expected_body = response[2]
1557 if bool(expected_body) != bool(body):
1558 # Not expecting a body and provided one
1559 # or expecting a body and not provided one.
1560 raise UnexpectedBodyError(expected_body, body)
1561 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001562 expected_body = json.loads(expected_body)
1563 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001564 if body != expected_body:
1565 raise UnexpectedBodyError(expected_body, body)
1566 return HttpRequestMock(resp, content, postproc)
1567 elif self.check_unexpected:
1568 raise UnexpectedMethodError(methodId=methodId)
1569 else:
1570 model = JsonModel(False)
1571 return HttpRequestMock(None, '{}', model.response)
1572
1573
1574class HttpMock(object):
1575 """Mock of httplib2.Http"""
1576
1577 def __init__(self, filename=None, headers=None):
1578 """
1579 Args:
1580 filename: string, absolute filename to read response from
1581 headers: dict, header to return with response
1582 """
1583 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001584 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001585 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001586 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001587 self.data = f.read()
1588 f.close()
1589 else:
1590 self.data = None
1591 self.response_headers = headers
1592 self.headers = None
1593 self.uri = None
1594 self.method = None
1595 self.body = None
1596 self.headers = None
1597
1598
1599 def request(self, uri,
1600 method='GET',
1601 body=None,
1602 headers=None,
1603 redirections=1,
1604 connection_type=None):
1605 self.uri = uri
1606 self.method = method
1607 self.body = body
1608 self.headers = headers
1609 return httplib2.Response(self.response_headers), self.data
1610
1611
1612class HttpMockSequence(object):
1613 """Mock of httplib2.Http
1614
1615 Mocks a sequence of calls to request returning different responses for each
1616 call. Create an instance initialized with the desired response headers
1617 and content and then use as if an httplib2.Http instance.
1618
1619 http = HttpMockSequence([
1620 ({'status': '401'}, ''),
1621 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1622 ({'status': '200'}, 'echo_request_headers'),
1623 ])
1624 resp, content = http.request("http://examples.com")
1625
1626 There are special values you can pass in for content to trigger
1627 behavours that are helpful in testing.
1628
1629 'echo_request_headers' means return the request headers in the response body
1630 'echo_request_headers_as_json' means return the request headers in
1631 the response body
1632 'echo_request_body' means return the request body in the response body
1633 'echo_request_uri' means return the request uri in the response body
1634 """
1635
1636 def __init__(self, iterable):
1637 """
1638 Args:
1639 iterable: iterable, a sequence of pairs of (headers, body)
1640 """
1641 self._iterable = iterable
1642 self.follow_redirects = True
1643
1644 def request(self, uri,
1645 method='GET',
1646 body=None,
1647 headers=None,
1648 redirections=1,
1649 connection_type=None):
1650 resp, content = self._iterable.pop(0)
1651 if content == 'echo_request_headers':
1652 content = headers
1653 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001654 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001655 elif content == 'echo_request_body':
1656 if hasattr(body, 'read'):
1657 content = body.read()
1658 else:
1659 content = body
1660 elif content == 'echo_request_uri':
1661 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001662 if isinstance(content, six.text_type):
1663 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001664 return httplib2.Response(resp), content
1665
1666
1667def set_user_agent(http, user_agent):
1668 """Set the user-agent on every request.
1669
1670 Args:
1671 http - An instance of httplib2.Http
1672 or something that acts like it.
1673 user_agent: string, the value for the user-agent header.
1674
1675 Returns:
1676 A modified instance of http that was passed in.
1677
1678 Example:
1679
1680 h = httplib2.Http()
1681 h = set_user_agent(h, "my-app-name/6.0")
1682
1683 Most of the time the user-agent will be set doing auth, this is for the rare
1684 cases where you are accessing an unauthenticated endpoint.
1685 """
1686 request_orig = http.request
1687
1688 # The closure that will replace 'httplib2.Http.request'.
1689 def new_request(uri, method='GET', body=None, headers=None,
1690 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1691 connection_type=None):
1692 """Modify the request headers to add the user-agent."""
1693 if headers is None:
1694 headers = {}
1695 if 'user-agent' in headers:
1696 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1697 else:
1698 headers['user-agent'] = user_agent
1699 resp, content = request_orig(uri, method, body, headers,
1700 redirections, connection_type)
1701 return resp, content
1702
1703 http.request = new_request
1704 return http
1705
1706
1707def tunnel_patch(http):
1708 """Tunnel PATCH requests over POST.
1709 Args:
1710 http - An instance of httplib2.Http
1711 or something that acts like it.
1712
1713 Returns:
1714 A modified instance of http that was passed in.
1715
1716 Example:
1717
1718 h = httplib2.Http()
1719 h = tunnel_patch(h, "my-app-name/6.0")
1720
1721 Useful if you are running on a platform that doesn't support PATCH.
1722 Apply this last if you are using OAuth 1.0, as changing the method
1723 will result in a different signature.
1724 """
1725 request_orig = http.request
1726
1727 # The closure that will replace 'httplib2.Http.request'.
1728 def new_request(uri, method='GET', body=None, headers=None,
1729 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1730 connection_type=None):
1731 """Modify the request headers to add the user-agent."""
1732 if headers is None:
1733 headers = {}
1734 if method == 'PATCH':
1735 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001736 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001737 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1738 headers['x-http-method-override'] = "PATCH"
1739 method = 'POST'
1740 resp, content = request_orig(uri, method, body, headers,
1741 redirections, connection_type)
1742 return resp, content
1743
1744 http.request = new_request
1745 return http
Igor Maravić22435292017-01-19 22:28:22 +01001746
1747
1748def build_http():
1749 """Builds httplib2.Http object
1750
1751 Returns:
1752 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1753 To override default timeout call
1754
1755 socket.setdefaulttimeout(timeout_in_sec)
1756
1757 before interacting with this method.
1758 """
1759 if socket.getdefaulttimeout() is not None:
1760 http_timeout = socket.getdefaulttimeout()
1761 else:
1762 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1763 return httplib2.Http(timeout=http_timeout)