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