blob: 29aef0e89f62d34eb6298161509cb1b75f6c9647 [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Classes to encapsulate a single HTTP request.
16
17The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actuall HTTP request.
20"""
INADA Naoki0bceb332014-08-20 15:27:52 +090021from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090022import six
eesheeshc6425a02016-02-12 15:07:06 +000023from six.moves import http_client
INADA Naokie4ea1a92015-03-04 03:45:42 +090024from six.moves import range
John Asmuth864311d2014-04-24 15:46:08 -040025
26__author__ = 'jcgregorio@google.com (Joe Gregorio)'
27
Pat Ferateed9affd2015-03-03 16:03:15 -080028from six import BytesIO, StringIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080029from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
Pat Ferateed9affd2015-03-03 16:03:15 -080030
John Asmuth864311d2014-04-24 15:46:08 -040031import base64
32import copy
33import gzip
34import httplib2
Craig Citro6ae34d72014-08-18 23:10:09 -070035import json
John Asmuth864311d2014-04-24 15:46:08 -040036import logging
John Asmuth864311d2014-04-24 15:46:08 -040037import mimetypes
38import os
39import random
eesheeshc6425a02016-02-12 15:07:06 +000040import socket
John Asmuth864311d2014-04-24 15:46:08 -040041import sys
42import time
John Asmuth864311d2014-04-24 15:46:08 -040043import uuid
44
Tay Ray Chuan3146c922016-04-20 16:38:19 +000045# TODO(issue 221): Remove this conditional import jibbajabba.
46try:
47 import ssl
48except ImportError:
49 _ssl_SSLError = object()
50else:
51 _ssl_SSLError = ssl.SSLError
52
John Asmuth864311d2014-04-24 15:46:08 -040053from email.generator import Generator
54from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
56from email.parser import FeedParser
Pat Ferateb240c172015-03-03 16:23:51 -080057
Helen Koikede13e3b2018-04-26 16:05:16 -030058from googleapiclient import _helpers as util
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070059
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -070060from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080061from googleapiclient.errors import BatchError
62from googleapiclient.errors import HttpError
63from googleapiclient.errors import InvalidChunkSizeError
64from googleapiclient.errors import ResumableUploadError
65from googleapiclient.errors import UnexpectedBodyError
66from googleapiclient.errors import UnexpectedMethodError
67from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040068
69
Emmett Butler09699152016-02-08 14:26:00 -080070LOGGER = logging.getLogger(__name__)
71
Craig Citrod1aa6912018-02-28 15:31:08 -080072DEFAULT_CHUNK_SIZE = 100*1024*1024
John Asmuth864311d2014-04-24 15:46:08 -040073
74MAX_URI_LENGTH = 2048
75
eesheeshc6425a02016-02-12 15:07:06 +000076_TOO_MANY_REQUESTS = 429
77
Igor Maravić22435292017-01-19 22:28:22 +010078DEFAULT_HTTP_TIMEOUT_SEC = 60
79
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -070080_LEGACY_BATCH_URI = 'https://www.googleapis.com/batch'
81
eesheeshc6425a02016-02-12 15:07:06 +000082
83def _should_retry_response(resp_status, content):
84 """Determines whether a response should be retried.
85
86 Args:
87 resp_status: The response status received.
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -080088 content: The response content body.
eesheeshc6425a02016-02-12 15:07:06 +000089
90 Returns:
91 True if the response should be retried, otherwise False.
92 """
93 # Retry on 5xx errors.
94 if resp_status >= 500:
95 return True
96
97 # Retry on 429 errors.
98 if resp_status == _TOO_MANY_REQUESTS:
99 return True
100
101 # For 403 errors, we have to check for the `reason` in the response to
102 # determine if we should retry.
103 if resp_status == six.moves.http_client.FORBIDDEN:
104 # If there's no details about the 403 type, don't retry.
105 if not content:
106 return False
107
108 # Content is in JSON format.
109 try:
110 data = json.loads(content.decode('utf-8'))
Nilayan Bhattacharya90ffb852017-12-05 15:30:32 -0800111 if isinstance(data, dict):
112 reason = data['error']['errors'][0]['reason']
113 else:
114 reason = data[0]['error']['errors']['reason']
eesheeshc6425a02016-02-12 15:07:06 +0000115 except (UnicodeDecodeError, ValueError, KeyError):
116 LOGGER.warning('Invalid JSON content from response: %s', content)
117 return False
118
119 LOGGER.warning('Encountered 403 Forbidden with reason "%s"', reason)
120
121 # Only retry on rate limit related failures.
122 if reason in ('userRateLimitExceeded', 'rateLimitExceeded', ):
123 return True
124
125 # Everything else is a success or non-retriable so break.
126 return False
127
John Asmuth864311d2014-04-24 15:46:08 -0400128
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100129def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
130 **kwargs):
131 """Retries an HTTP request multiple times while handling errors.
132
133 If after all retries the request still fails, last error is either returned as
134 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
135
136 Args:
137 http: Http object to be used to execute request.
138 num_retries: Maximum number of retries.
139 req_type: Type of the request (used for logging retries).
140 sleep, rand: Functions to sleep for random time between retries.
141 uri: URI to be requested.
142 method: HTTP method to be used.
143 args, kwargs: Additional arguments passed to http.request.
144
145 Returns:
146 resp, content - Response from the http request (may be HTTP 5xx).
147 """
148 resp = None
eesheeshc6425a02016-02-12 15:07:06 +0000149 content = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100150 for retry_num in range(num_retries + 1):
151 if retry_num > 0:
eesheeshc6425a02016-02-12 15:07:06 +0000152 # Sleep before retrying.
153 sleep_time = rand() * 2 ** retry_num
Emmett Butler09699152016-02-08 14:26:00 -0800154 LOGGER.warning(
eesheeshc6425a02016-02-12 15:07:06 +0000155 'Sleeping %.2f seconds before retry %d of %d for %s: %s %s, after %s',
156 sleep_time, retry_num, num_retries, req_type, method, uri,
157 resp.status if resp else exception)
158 sleep(sleep_time)
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100159
160 try:
eesheeshc6425a02016-02-12 15:07:06 +0000161 exception = None
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100162 resp, content = http.request(uri, method, *args, **kwargs)
eesheeshc6425a02016-02-12 15:07:06 +0000163 # Retry on SSL errors and socket timeout errors.
Tay Ray Chuan3146c922016-04-20 16:38:19 +0000164 except _ssl_SSLError as ssl_error:
eesheeshc6425a02016-02-12 15:07:06 +0000165 exception = ssl_error
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 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300206 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400207 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 """
andrewnestera4a44cf2017-03-31 16:09:31 +0300232 if self.total_size is not None and self.total_size != 0:
John Asmuth864311d2014-04-24 15:46:08 -0400233 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
John Asmuth864311d2014-04-24 15:46:08 -0400512 media = MediaFileUpload('cow.png', mimetype='image/png',
513 chunksize=1024*1024, resumable=True)
514 farm.animals().insert(
515 id='cow',
516 name='cow.png',
517 media_body=media).execute()
518
519 Depending on the platform you are working on, you may pass -1 as the
520 chunksize, which indicates that the entire file should be uploaded in a single
521 request. If the underlying platform supports streams, such as Python 2.6 or
522 later, then this can be very efficient as it avoids multiple connections, and
523 also avoids loading the entire file into memory before sending it. Note that
524 Google App Engine has a 5MB limit on request size, so you should never set
525 your chunksize larger than 5MB, or to -1.
526 """
527
528 @util.positional(2)
529 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
530 resumable=False):
531 """Constructor.
532
533 Args:
534 filename: string, Name of the file.
535 mimetype: string, Mime-type of the file. If None then a mime-type will be
536 guessed from the file extension.
537 chunksize: int, File will be uploaded in chunks of this many bytes. Only
538 used if resumable=True. Pass in a value of -1 if the file is to be
539 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
540 on request size, so you should never set your chunksize larger than 5MB,
541 or to -1.
542 resumable: bool, True if this is a resumable upload. False means upload
543 in a single request.
544 """
545 self._filename = filename
546 fd = open(self._filename, 'rb')
547 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800548 # No mimetype provided, make a guess.
549 mimetype, _ = mimetypes.guess_type(filename)
550 if mimetype is None:
551 # Guess failed, use octet-stream.
552 mimetype = 'application/octet-stream'
John Asmuth864311d2014-04-24 15:46:08 -0400553 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
554 resumable=resumable)
555
556 def to_json(self):
557 """Creating a JSON representation of an instance of MediaFileUpload.
558
559 Returns:
560 string, a JSON representation of this instance, suitable to pass to
561 from_json().
562 """
563 return self._to_json(strip=['_fd'])
564
565 @staticmethod
566 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700567 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400568 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
569 chunksize=d['_chunksize'], resumable=d['_resumable'])
570
571
572class MediaInMemoryUpload(MediaIoBaseUpload):
573 """MediaUpload for a chunk of bytes.
574
575 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
576 the stream.
577 """
578
579 @util.positional(2)
580 def __init__(self, body, mimetype='application/octet-stream',
581 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
582 """Create a new MediaInMemoryUpload.
583
584 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
585 the stream.
586
587 Args:
588 body: string, Bytes of body content.
589 mimetype: string, Mime-type of the file or default of
590 'application/octet-stream'.
591 chunksize: int, File will be uploaded in chunks of this many bytes. Only
592 used if resumable=True.
593 resumable: bool, True if this is a resumable upload. False means upload
594 in a single request.
595 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800596 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400597 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
598 resumable=resumable)
599
600
601class MediaIoBaseDownload(object):
602 """"Download media resources.
603
604 Note that the Python file object is compatible with io.Base and can be used
605 with this class also.
606
607
608 Example:
609 request = farms.animals().get_media(id='cow')
610 fh = io.FileIO('cow.png', mode='wb')
611 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
612
613 done = False
614 while done is False:
615 status, done = downloader.next_chunk()
616 if status:
617 print "Download %d%%." % int(status.progress() * 100)
618 print "Download Complete!"
619 """
620
621 @util.positional(3)
622 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
623 """Constructor.
624
625 Args:
626 fd: io.Base or file object, The stream in which to write the downloaded
627 bytes.
628 request: googleapiclient.http.HttpRequest, the media request to perform in
629 chunks.
630 chunksize: int, File will be downloaded in chunks of this many bytes.
631 """
632 self._fd = fd
633 self._request = request
634 self._uri = request.uri
635 self._chunksize = chunksize
636 self._progress = 0
637 self._total_size = None
638 self._done = False
639
640 # Stubs for testing.
641 self._sleep = time.sleep
642 self._rand = random.random
643
644 @util.positional(1)
645 def next_chunk(self, num_retries=0):
646 """Get the next chunk of the download.
647
648 Args:
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500649 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400650 exponential backoff. If all retries fail, the raised HttpError
651 represents the last request. If zero (default), we attempt the
652 request only once.
653
654 Returns:
Nilayan Bhattacharya89906ac2017-10-27 13:47:23 -0700655 (status, done): (MediaDownloadProgress, boolean)
John Asmuth864311d2014-04-24 15:46:08 -0400656 The value of 'done' will be True when the media has been fully
Daniel44067782018-01-16 23:17:56 +0100657 downloaded or the total size of the media is unknown.
John Asmuth864311d2014-04-24 15:46:08 -0400658
659 Raises:
660 googleapiclient.errors.HttpError if the response was not a 2xx.
661 httplib2.HttpLib2Error if a transport error has occured.
662 """
663 headers = {
664 'range': 'bytes=%d-%d' % (
665 self._progress, self._progress + self._chunksize)
666 }
667 http = self._request.http
668
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100669 resp, content = _retry_request(
670 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
671 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400672
673 if resp.status in [200, 206]:
674 if 'content-location' in resp and resp['content-location'] != self._uri:
675 self._uri = resp['content-location']
676 self._progress += len(content)
677 self._fd.write(content)
678
679 if 'content-range' in resp:
680 content_range = resp['content-range']
681 length = content_range.rsplit('/', 1)[1]
682 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800683 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800684 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400685
Daniel44067782018-01-16 23:17:56 +0100686 if self._total_size is None or self._progress == self._total_size:
John Asmuth864311d2014-04-24 15:46:08 -0400687 self._done = True
688 return MediaDownloadProgress(self._progress, self._total_size), self._done
689 else:
690 raise HttpError(resp, content, uri=self._uri)
691
692
693class _StreamSlice(object):
694 """Truncated stream.
695
696 Takes a stream and presents a stream that is a slice of the original stream.
697 This is used when uploading media in chunks. In later versions of Python a
698 stream can be passed to httplib in place of the string of data to send. The
699 problem is that httplib just blindly reads to the end of the stream. This
700 wrapper presents a virtual stream that only reads to the end of the chunk.
701 """
702
703 def __init__(self, stream, begin, chunksize):
704 """Constructor.
705
706 Args:
707 stream: (io.Base, file object), the stream to wrap.
708 begin: int, the seek position the chunk begins at.
709 chunksize: int, the size of the chunk.
710 """
711 self._stream = stream
712 self._begin = begin
713 self._chunksize = chunksize
714 self._stream.seek(begin)
715
716 def read(self, n=-1):
717 """Read n bytes.
718
719 Args:
720 n, int, the number of bytes to read.
721
722 Returns:
723 A string of length 'n', or less if EOF is reached.
724 """
725 # The data left available to read sits in [cur, end)
726 cur = self._stream.tell()
727 end = self._begin + self._chunksize
728 if n == -1 or cur + n > end:
729 n = end - cur
730 return self._stream.read(n)
731
732
733class HttpRequest(object):
734 """Encapsulates a single HTTP request."""
735
736 @util.positional(4)
737 def __init__(self, http, postproc, uri,
738 method='GET',
739 body=None,
740 headers=None,
741 methodId=None,
742 resumable=None):
743 """Constructor for an HttpRequest.
744
745 Args:
746 http: httplib2.Http, the transport object to use to make a request
747 postproc: callable, called on the HTTP response and content to transform
748 it into a data object before returning, or raising an exception
749 on an error.
750 uri: string, the absolute URI to send the request to
751 method: string, the HTTP method to use
752 body: string, the request body of the HTTP request,
753 headers: dict, the HTTP request headers
754 methodId: string, a unique identifier for the API method being called.
755 resumable: MediaUpload, None if this is not a resumbale request.
756 """
757 self.uri = uri
758 self.method = method
759 self.body = body
760 self.headers = headers or {}
761 self.methodId = methodId
762 self.http = http
763 self.postproc = postproc
764 self.resumable = resumable
765 self.response_callbacks = []
766 self._in_error_state = False
767
John Asmuth864311d2014-04-24 15:46:08 -0400768 # The size of the non-media part of the request.
769 self.body_size = len(self.body or '')
770
771 # The resumable URI to send chunks to.
772 self.resumable_uri = None
773
774 # The bytes that have been uploaded.
775 self.resumable_progress = 0
776
777 # Stubs for testing.
778 self._rand = random.random
779 self._sleep = time.sleep
780
781 @util.positional(1)
782 def execute(self, http=None, num_retries=0):
783 """Execute the request.
784
785 Args:
786 http: httplib2.Http, an http object to be used in place of the
787 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500788 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400789 exponential backoff. If all retries fail, the raised HttpError
790 represents the last request. If zero (default), we attempt the
791 request only once.
792
793 Returns:
794 A deserialized object model of the response body as determined
795 by the postproc.
796
797 Raises:
798 googleapiclient.errors.HttpError if the response was not a 2xx.
799 httplib2.HttpLib2Error if a transport error has occured.
800 """
801 if http is None:
802 http = self.http
803
804 if self.resumable:
805 body = None
806 while body is None:
807 _, body = self.next_chunk(http=http, num_retries=num_retries)
808 return body
809
810 # Non-resumable case.
811
812 if 'content-length' not in self.headers:
813 self.headers['content-length'] = str(self.body_size)
814 # If the request URI is too long then turn it into a POST request.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800815 # Assume that a GET request never contains a request body.
John Asmuth864311d2014-04-24 15:46:08 -0400816 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
817 self.method = 'POST'
818 self.headers['x-http-method-override'] = 'GET'
819 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800820 parsed = urlparse(self.uri)
821 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400822 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
823 None)
824 )
825 self.body = parsed.query
826 self.headers['content-length'] = str(len(self.body))
827
828 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100829 resp, content = _retry_request(
830 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
831 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400832
833 for callback in self.response_callbacks:
834 callback(resp)
835 if resp.status >= 300:
836 raise HttpError(resp, content, uri=self.uri)
837 return self.postproc(resp, content)
838
839 @util.positional(2)
840 def add_response_callback(self, cb):
841 """add_response_headers_callback
842
843 Args:
844 cb: Callback to be called on receiving the response headers, of signature:
845
846 def cb(resp):
847 # Where resp is an instance of httplib2.Response
848 """
849 self.response_callbacks.append(cb)
850
851 @util.positional(1)
852 def next_chunk(self, http=None, num_retries=0):
853 """Execute the next step of a resumable upload.
854
855 Can only be used if the method being executed supports media uploads and
856 the MediaUpload object passed in was flagged as using resumable upload.
857
858 Example:
859
860 media = MediaFileUpload('cow.png', mimetype='image/png',
861 chunksize=1000, resumable=True)
862 request = farm.animals().insert(
863 id='cow',
864 name='cow.png',
865 media_body=media)
866
867 response = None
868 while response is None:
869 status, response = request.next_chunk()
870 if status:
871 print "Upload %d%% complete." % int(status.progress() * 100)
872
873
874 Args:
875 http: httplib2.Http, an http object to be used in place of the
876 one the HttpRequest request object was constructed with.
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500877 num_retries: Integer, number of times to retry with randomized
John Asmuth864311d2014-04-24 15:46:08 -0400878 exponential backoff. If all retries fail, the raised HttpError
879 represents the last request. If zero (default), we attempt the
880 request only once.
881
882 Returns:
883 (status, body): (ResumableMediaStatus, object)
884 The body will be None until the resumable media is fully uploaded.
885
886 Raises:
887 googleapiclient.errors.HttpError if the response was not a 2xx.
888 httplib2.HttpLib2Error if a transport error has occured.
889 """
890 if http is None:
891 http = self.http
892
893 if self.resumable.size() is None:
894 size = '*'
895 else:
896 size = str(self.resumable.size())
897
898 if self.resumable_uri is None:
899 start_headers = copy.copy(self.headers)
900 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
901 if size != '*':
902 start_headers['X-Upload-Content-Length'] = size
903 start_headers['content-length'] = str(self.body_size)
904
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100905 resp, content = _retry_request(
906 http, num_retries, 'resumable URI request', self._sleep, self._rand,
907 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400908
909 if resp.status == 200 and 'location' in resp:
910 self.resumable_uri = resp['location']
911 else:
912 raise ResumableUploadError(resp, content)
913 elif self._in_error_state:
914 # If we are in an error state then query the server for current state of
915 # the upload by sending an empty PUT and reading the 'range' header in
916 # the response.
917 headers = {
918 'Content-Range': 'bytes */%s' % size,
919 'content-length': '0'
920 }
921 resp, content = http.request(self.resumable_uri, 'PUT',
922 headers=headers)
923 status, body = self._process_response(resp, content)
924 if body:
925 # The upload was complete.
926 return (status, body)
927
e00Efafe8582015-10-10 18:19:37 +0200928 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400929 data = self.resumable.stream()
930 if self.resumable.chunksize() == -1:
931 data.seek(self.resumable_progress)
932 chunk_end = self.resumable.size() - self.resumable_progress - 1
933 else:
934 # Doing chunking with a stream, so wrap a slice of the stream.
935 data = _StreamSlice(data, self.resumable_progress,
936 self.resumable.chunksize())
937 chunk_end = min(
938 self.resumable_progress + self.resumable.chunksize() - 1,
939 self.resumable.size() - 1)
940 else:
941 data = self.resumable.getbytes(
942 self.resumable_progress, self.resumable.chunksize())
943
944 # A short read implies that we are at EOF, so finish the upload.
945 if len(data) < self.resumable.chunksize():
946 size = str(self.resumable_progress + len(data))
947
948 chunk_end = self.resumable_progress + len(data) - 1
949
950 headers = {
951 'Content-Range': 'bytes %d-%d/%s' % (
952 self.resumable_progress, chunk_end, size),
953 # Must set the content-length header here because httplib can't
954 # calculate the size when working with _StreamSlice.
955 'Content-Length': str(chunk_end - self.resumable_progress + 1)
956 }
957
INADA Naokie4ea1a92015-03-04 03:45:42 +0900958 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400959 if retry_num > 0:
960 self._sleep(self._rand() * 2**retry_num)
Emmett Butler09699152016-02-08 14:26:00 -0800961 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -0400962 'Retry #%d for media upload: %s %s, following status: %d'
963 % (retry_num, self.method, self.uri, resp.status))
964
965 try:
966 resp, content = http.request(self.resumable_uri, method='PUT',
967 body=data,
968 headers=headers)
969 except:
970 self._in_error_state = True
971 raise
Zhihao Yuancc6d3982016-07-27 11:40:45 -0500972 if not _should_retry_response(resp.status, content):
John Asmuth864311d2014-04-24 15:46:08 -0400973 break
974
975 return self._process_response(resp, content)
976
977 def _process_response(self, resp, content):
978 """Process the response from a single chunk upload.
979
980 Args:
981 resp: httplib2.Response, the response object.
982 content: string, the content of the response.
983
984 Returns:
985 (status, body): (ResumableMediaStatus, object)
986 The body will be None until the resumable media is fully uploaded.
987
988 Raises:
989 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
990 """
991 if resp.status in [200, 201]:
992 self._in_error_state = False
993 return None, self.postproc(resp, content)
994 elif resp.status == 308:
995 self._in_error_state = False
996 # A "308 Resume Incomplete" indicates we are not done.
Matt Carroll94a53942016-12-20 13:56:43 -0800997 try:
998 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
999 except KeyError:
1000 # If resp doesn't contain range header, resumable progress is 0
1001 self.resumable_progress = 0
John Asmuth864311d2014-04-24 15:46:08 -04001002 if 'location' in resp:
1003 self.resumable_uri = resp['location']
1004 else:
1005 self._in_error_state = True
1006 raise HttpError(resp, content, uri=self.uri)
1007
1008 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
1009 None)
1010
1011 def to_json(self):
1012 """Returns a JSON representation of the HttpRequest."""
1013 d = copy.copy(self.__dict__)
1014 if d['resumable'] is not None:
1015 d['resumable'] = self.resumable.to_json()
1016 del d['http']
1017 del d['postproc']
1018 del d['_sleep']
1019 del d['_rand']
1020
Craig Citro6ae34d72014-08-18 23:10:09 -07001021 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -04001022
1023 @staticmethod
1024 def from_json(s, http, postproc):
1025 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -07001026 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -04001027 if d['resumable'] is not None:
1028 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
1029 return HttpRequest(
1030 http,
1031 postproc,
1032 uri=d['uri'],
1033 method=d['method'],
1034 body=d['body'],
1035 headers=d['headers'],
1036 methodId=d['methodId'],
1037 resumable=d['resumable'])
1038
1039
1040class BatchHttpRequest(object):
1041 """Batches multiple HttpRequest objects into a single HTTP request.
1042
1043 Example:
1044 from googleapiclient.http import BatchHttpRequest
1045
1046 def list_animals(request_id, response, exception):
1047 \"\"\"Do something with the animals list response.\"\"\"
1048 if exception is not None:
1049 # Do something with the exception.
1050 pass
1051 else:
1052 # Do something with the response.
1053 pass
1054
1055 def list_farmers(request_id, response, exception):
1056 \"\"\"Do something with the farmers list response.\"\"\"
1057 if exception is not None:
1058 # Do something with the exception.
1059 pass
1060 else:
1061 # Do something with the response.
1062 pass
1063
1064 service = build('farm', 'v2')
1065
1066 batch = BatchHttpRequest()
1067
1068 batch.add(service.animals().list(), list_animals)
1069 batch.add(service.farmers().list(), list_farmers)
1070 batch.execute(http=http)
1071 """
1072
1073 @util.positional(1)
1074 def __init__(self, callback=None, batch_uri=None):
1075 """Constructor for a BatchHttpRequest.
1076
1077 Args:
1078 callback: callable, A callback to be called for each response, of the
1079 form callback(id, response, exception). The first parameter is the
1080 request id, and the second is the deserialized response object. The
1081 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1082 occurred while processing the request, or None if no error occurred.
1083 batch_uri: string, URI to send batch requests to.
1084 """
1085 if batch_uri is None:
Jon Wayne Parrottbae748a2018-03-28 10:21:12 -07001086 batch_uri = _LEGACY_BATCH_URI
1087
1088 if batch_uri == _LEGACY_BATCH_URI:
1089 LOGGER.warn(
1090 "You have constructed a BatchHttpRequest using the legacy batch "
1091 "endpoint %s. This endpoint will be turned down on March 25, 2019. "
1092 "Please provide the API-specific endpoint or use "
1093 "service.new_batch_http_request(). For more details see "
1094 "https://developers.googleblog.com/2018/03/discontinuing-support-for-json-rpc-and.html"
1095 "and https://developers.google.com/api-client-library/python/guide/batch.",
1096 _LEGACY_BATCH_URI)
John Asmuth864311d2014-04-24 15:46:08 -04001097 self._batch_uri = batch_uri
1098
1099 # Global callback to be called for each individual response in the batch.
1100 self._callback = callback
1101
1102 # A map from id to request.
1103 self._requests = {}
1104
1105 # A map from id to callback.
1106 self._callbacks = {}
1107
1108 # List of request ids, in the order in which they were added.
1109 self._order = []
1110
1111 # The last auto generated id.
1112 self._last_auto_id = 0
1113
1114 # Unique ID on which to base the Content-ID headers.
1115 self._base_id = None
1116
1117 # A map from request id to (httplib2.Response, content) response pairs
1118 self._responses = {}
1119
1120 # A map of id(Credentials) that have been refreshed.
1121 self._refreshed_credentials = {}
1122
1123 def _refresh_and_apply_credentials(self, request, http):
1124 """Refresh the credentials and apply to the request.
1125
1126 Args:
1127 request: HttpRequest, the request.
1128 http: httplib2.Http, the global http object for the batch.
1129 """
1130 # For the credentials to refresh, but only once per refresh_token
1131 # If there is no http per the request then refresh the http passed in
1132 # via execute()
1133 creds = None
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001134 request_credentials = False
1135
1136 if request.http is not None:
1137 creds = _auth.get_credentials_from_http(request.http)
1138 request_credentials = True
1139
1140 if creds is None and http is not None:
1141 creds = _auth.get_credentials_from_http(http)
1142
John Asmuth864311d2014-04-24 15:46:08 -04001143 if creds is not None:
1144 if id(creds) not in self._refreshed_credentials:
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001145 _auth.refresh_credentials(creds)
John Asmuth864311d2014-04-24 15:46:08 -04001146 self._refreshed_credentials[id(creds)] = 1
1147
1148 # Only apply the credentials if we are using the http object passed in,
1149 # otherwise apply() will get called during _serialize_request().
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001150 if request.http is None or not request_credentials:
1151 _auth.apply_credentials(creds, request.headers)
1152
John Asmuth864311d2014-04-24 15:46:08 -04001153
1154 def _id_to_header(self, id_):
1155 """Convert an id to a Content-ID header value.
1156
1157 Args:
1158 id_: string, identifier of individual request.
1159
1160 Returns:
1161 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1162 the value because Content-ID headers are supposed to be universally
1163 unique.
1164 """
1165 if self._base_id is None:
1166 self._base_id = uuid.uuid4()
1167
Pat Ferated5b61bd2015-03-03 16:04:11 -08001168 return '<%s+%s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001169
1170 def _header_to_id(self, header):
1171 """Convert a Content-ID header value to an id.
1172
1173 Presumes the Content-ID header conforms to the format that _id_to_header()
1174 returns.
1175
1176 Args:
1177 header: string, Content-ID header value.
1178
1179 Returns:
1180 The extracted id value.
1181
1182 Raises:
1183 BatchError if the header is not in the expected format.
1184 """
1185 if header[0] != '<' or header[-1] != '>':
1186 raise BatchError("Invalid value for Content-ID: %s" % header)
1187 if '+' not in header:
1188 raise BatchError("Invalid value for Content-ID: %s" % header)
1189 base, id_ = header[1:-1].rsplit('+', 1)
1190
Pat Ferated5b61bd2015-03-03 16:04:11 -08001191 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001192
1193 def _serialize_request(self, request):
1194 """Convert an HttpRequest object into a string.
1195
1196 Args:
1197 request: HttpRequest, the request to serialize.
1198
1199 Returns:
1200 The request as a string in application/http format.
1201 """
1202 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001203 parsed = urlparse(request.uri)
1204 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001205 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001206 )
1207 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1208 major, minor = request.headers.get('content-type', 'application/json').split('/')
1209 msg = MIMENonMultipart(major, minor)
1210 headers = request.headers.copy()
1211
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001212 if request.http is not None:
1213 credentials = _auth.get_credentials_from_http(request.http)
1214 if credentials is not None:
1215 _auth.apply_credentials(credentials, headers)
John Asmuth864311d2014-04-24 15:46:08 -04001216
1217 # MIMENonMultipart adds its own Content-Type header.
1218 if 'content-type' in headers:
1219 del headers['content-type']
1220
INADA Naokie4ea1a92015-03-04 03:45:42 +09001221 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001222 msg[key] = value
1223 msg['Host'] = parsed.netloc
1224 msg.set_unixfrom(None)
1225
1226 if request.body is not None:
1227 msg.set_payload(request.body)
1228 msg['content-length'] = str(len(request.body))
1229
1230 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001231 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001232 # maxheaderlen=0 means don't line wrap headers.
1233 g = Generator(fp, maxheaderlen=0)
1234 g.flatten(msg, unixfrom=False)
1235 body = fp.getvalue()
1236
Pat Feratec9abbbd2015-03-03 18:00:38 -08001237 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001238
1239 def _deserialize_response(self, payload):
1240 """Convert string into httplib2 response and content.
1241
1242 Args:
1243 payload: string, headers and body as a string.
1244
1245 Returns:
1246 A pair (resp, content), such as would be returned from httplib2.request.
1247 """
1248 # Strip off the status line
1249 status_line, payload = payload.split('\n', 1)
1250 protocol, status, reason = status_line.split(' ', 2)
1251
1252 # Parse the rest of the response
1253 parser = FeedParser()
1254 parser.feed(payload)
1255 msg = parser.close()
1256 msg['status'] = status
1257
1258 # Create httplib2.Response from the parsed headers.
1259 resp = httplib2.Response(msg)
1260 resp.reason = reason
1261 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1262
1263 content = payload.split('\r\n\r\n', 1)[1]
1264
1265 return resp, content
1266
1267 def _new_id(self):
1268 """Create a new id.
1269
1270 Auto incrementing number that avoids conflicts with ids already used.
1271
1272 Returns:
1273 string, a new unique id.
1274 """
1275 self._last_auto_id += 1
1276 while str(self._last_auto_id) in self._requests:
1277 self._last_auto_id += 1
1278 return str(self._last_auto_id)
1279
1280 @util.positional(2)
1281 def add(self, request, callback=None, request_id=None):
1282 """Add a new request.
1283
1284 Every callback added will be paired with a unique id, the request_id. That
1285 unique id will be passed back to the callback when the response comes back
1286 from the server. The default behavior is to have the library generate it's
1287 own unique id. If the caller passes in a request_id then they must ensure
1288 uniqueness for each request_id, and if they are not an exception is
1289 raised. Callers should either supply all request_ids or nevery supply a
1290 request id, to avoid such an error.
1291
1292 Args:
1293 request: HttpRequest, Request to add to the batch.
1294 callback: callable, A callback to be called for this response, of the
1295 form callback(id, response, exception). The first parameter is the
1296 request id, and the second is the deserialized response object. The
1297 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1298 occurred while processing the request, or None if no errors occurred.
1299 request_id: string, A unique id for the request. The id will be passed to
1300 the callback with the response.
1301
1302 Returns:
1303 None
1304
1305 Raises:
1306 BatchError if a media request is added to a batch.
1307 KeyError is the request_id is not unique.
1308 """
1309 if request_id is None:
1310 request_id = self._new_id()
1311 if request.resumable is not None:
1312 raise BatchError("Media requests cannot be used in a batch request.")
1313 if request_id in self._requests:
1314 raise KeyError("A request with this ID already exists: %s" % request_id)
1315 self._requests[request_id] = request
1316 self._callbacks[request_id] = callback
1317 self._order.append(request_id)
1318
1319 def _execute(self, http, order, requests):
1320 """Serialize batch request, send to server, process response.
1321
1322 Args:
1323 http: httplib2.Http, an http object to be used to make the request with.
1324 order: list, list of request ids in the order they were added to the
1325 batch.
1326 request: list, list of request objects to send.
1327
1328 Raises:
1329 httplib2.HttpLib2Error if a transport error has occured.
1330 googleapiclient.errors.BatchError if the response is the wrong format.
1331 """
1332 message = MIMEMultipart('mixed')
1333 # Message should not write out it's own headers.
1334 setattr(message, '_write_headers', lambda self: None)
1335
1336 # Add all the individual requests.
1337 for request_id in order:
1338 request = requests[request_id]
1339
1340 msg = MIMENonMultipart('application', 'http')
1341 msg['Content-Transfer-Encoding'] = 'binary'
1342 msg['Content-ID'] = self._id_to_header(request_id)
1343
1344 body = self._serialize_request(request)
1345 msg.set_payload(body)
1346 message.attach(msg)
1347
Craig Citro72389b72014-07-15 17:12:50 -07001348 # encode the body: note that we can't use `as_string`, because
1349 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001350 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001351 g = Generator(fp, mangle_from_=False)
1352 g.flatten(message, unixfrom=False)
1353 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001354
1355 headers = {}
1356 headers['content-type'] = ('multipart/mixed; '
1357 'boundary="%s"') % message.get_boundary()
1358
1359 resp, content = http.request(self._batch_uri, method='POST', body=body,
1360 headers=headers)
1361
1362 if resp.status >= 300:
1363 raise HttpError(resp, content, uri=self._batch_uri)
1364
John Asmuth864311d2014-04-24 15:46:08 -04001365 # Prepend with a content-type header so FeedParser can handle it.
1366 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001367 # PY3's FeedParser only accepts unicode. So we should decode content
1368 # here, and encode each payload again.
1369 if six.PY3:
1370 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001371 for_parser = header + content
1372
1373 parser = FeedParser()
1374 parser.feed(for_parser)
1375 mime_response = parser.close()
1376
1377 if not mime_response.is_multipart():
1378 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1379 content=content)
1380
1381 for part in mime_response.get_payload():
1382 request_id = self._header_to_id(part['Content-ID'])
1383 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001384 # We encode content here to emulate normal http response.
1385 if isinstance(content, six.text_type):
1386 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001387 self._responses[request_id] = (response, content)
1388
1389 @util.positional(1)
1390 def execute(self, http=None):
1391 """Execute all the requests as a single batched HTTP request.
1392
1393 Args:
1394 http: httplib2.Http, an http object to be used in place of the one the
1395 HttpRequest request object was constructed with. If one isn't supplied
1396 then use a http object from the requests in this batch.
1397
1398 Returns:
1399 None
1400
1401 Raises:
1402 httplib2.HttpLib2Error if a transport error has occured.
1403 googleapiclient.errors.BatchError if the response is the wrong format.
1404 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001405 # If we have no requests return
1406 if len(self._order) == 0:
1407 return None
John Asmuth864311d2014-04-24 15:46:08 -04001408
1409 # If http is not supplied use the first valid one given in the requests.
1410 if http is None:
1411 for request_id in self._order:
1412 request = self._requests[request_id]
1413 if request is not None:
1414 http = request.http
1415 break
1416
1417 if http is None:
1418 raise ValueError("Missing a valid http object.")
1419
Gabriel Garcia23174be2016-05-25 17:28:07 +02001420 # Special case for OAuth2Credentials-style objects which have not yet been
1421 # refreshed with an initial access_token.
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001422 creds = _auth.get_credentials_from_http(http)
1423 if creds is not None:
1424 if not _auth.is_valid(creds):
Gabriel Garcia23174be2016-05-25 17:28:07 +02001425 LOGGER.info('Attempting refresh to obtain initial access_token')
Jon Wayne Parrottd3a5cf42017-06-19 17:55:04 -07001426 _auth.refresh_credentials(creds)
Gabriel Garcia23174be2016-05-25 17:28:07 +02001427
John Asmuth864311d2014-04-24 15:46:08 -04001428 self._execute(http, self._order, self._requests)
1429
1430 # Loop over all the requests and check for 401s. For each 401 request the
1431 # credentials should be refreshed and then sent again in a separate batch.
1432 redo_requests = {}
1433 redo_order = []
1434
1435 for request_id in self._order:
1436 resp, content = self._responses[request_id]
1437 if resp['status'] == '401':
1438 redo_order.append(request_id)
1439 request = self._requests[request_id]
1440 self._refresh_and_apply_credentials(request, http)
1441 redo_requests[request_id] = request
1442
1443 if redo_requests:
1444 self._execute(http, redo_order, redo_requests)
1445
1446 # Now process all callbacks that are erroring, and raise an exception for
1447 # ones that return a non-2xx response? Or add extra parameter to callback
1448 # that contains an HttpError?
1449
1450 for request_id in self._order:
1451 resp, content = self._responses[request_id]
1452
1453 request = self._requests[request_id]
1454 callback = self._callbacks[request_id]
1455
1456 response = None
1457 exception = None
1458 try:
1459 if resp.status >= 300:
1460 raise HttpError(resp, content, uri=request.uri)
1461 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001462 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001463 exception = e
1464
1465 if callback is not None:
1466 callback(request_id, response, exception)
1467 if self._callback is not None:
1468 self._callback(request_id, response, exception)
1469
1470
1471class HttpRequestMock(object):
1472 """Mock of HttpRequest.
1473
1474 Do not construct directly, instead use RequestMockBuilder.
1475 """
1476
1477 def __init__(self, resp, content, postproc):
1478 """Constructor for HttpRequestMock
1479
1480 Args:
1481 resp: httplib2.Response, the response to emulate coming from the request
1482 content: string, the response body
1483 postproc: callable, the post processing function usually supplied by
1484 the model class. See model.JsonModel.response() as an example.
1485 """
1486 self.resp = resp
1487 self.content = content
1488 self.postproc = postproc
1489 if resp is None:
1490 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1491 if 'reason' in self.resp:
1492 self.resp.reason = self.resp['reason']
1493
1494 def execute(self, http=None):
1495 """Execute the request.
1496
1497 Same behavior as HttpRequest.execute(), but the response is
1498 mocked and not really from an HTTP request/response.
1499 """
1500 return self.postproc(self.resp, self.content)
1501
1502
1503class RequestMockBuilder(object):
1504 """A simple mock of HttpRequest
1505
1506 Pass in a dictionary to the constructor that maps request methodIds to
1507 tuples of (httplib2.Response, content, opt_expected_body) that should be
1508 returned when that method is called. None may also be passed in for the
1509 httplib2.Response, in which case a 200 OK response will be generated.
1510 If an opt_expected_body (str or dict) is provided, it will be compared to
1511 the body and UnexpectedBodyError will be raised on inequality.
1512
1513 Example:
1514 response = '{"data": {"id": "tag:google.c...'
1515 requestBuilder = RequestMockBuilder(
1516 {
1517 'plus.activities.get': (None, response),
1518 }
1519 )
1520 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1521
1522 Methods that you do not supply a response for will return a
1523 200 OK with an empty string as the response content or raise an excpetion
1524 if check_unexpected is set to True. The methodId is taken from the rpcName
1525 in the discovery document.
1526
1527 For more details see the project wiki.
1528 """
1529
1530 def __init__(self, responses, check_unexpected=False):
1531 """Constructor for RequestMockBuilder
1532
1533 The constructed object should be a callable object
1534 that can replace the class HttpResponse.
1535
1536 responses - A dictionary that maps methodIds into tuples
1537 of (httplib2.Response, content). The methodId
1538 comes from the 'rpcName' field in the discovery
1539 document.
1540 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1541 should be raised on unsupplied method.
1542 """
1543 self.responses = responses
1544 self.check_unexpected = check_unexpected
1545
1546 def __call__(self, http, postproc, uri, method='GET', body=None,
1547 headers=None, methodId=None, resumable=None):
1548 """Implements the callable interface that discovery.build() expects
1549 of requestBuilder, which is to build an object compatible with
1550 HttpRequest.execute(). See that method for the description of the
1551 parameters and the expected response.
1552 """
1553 if methodId in self.responses:
1554 response = self.responses[methodId]
1555 resp, content = response[:2]
1556 if len(response) > 2:
1557 # Test the body against the supplied expected_body.
1558 expected_body = response[2]
1559 if bool(expected_body) != bool(body):
1560 # Not expecting a body and provided one
1561 # or expecting a body and not provided one.
1562 raise UnexpectedBodyError(expected_body, body)
1563 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001564 expected_body = json.loads(expected_body)
1565 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001566 if body != expected_body:
1567 raise UnexpectedBodyError(expected_body, body)
1568 return HttpRequestMock(resp, content, postproc)
1569 elif self.check_unexpected:
1570 raise UnexpectedMethodError(methodId=methodId)
1571 else:
1572 model = JsonModel(False)
1573 return HttpRequestMock(None, '{}', model.response)
1574
1575
1576class HttpMock(object):
1577 """Mock of httplib2.Http"""
1578
1579 def __init__(self, filename=None, headers=None):
1580 """
1581 Args:
1582 filename: string, absolute filename to read response from
1583 headers: dict, header to return with response
1584 """
1585 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001586 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001587 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001588 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001589 self.data = f.read()
1590 f.close()
1591 else:
1592 self.data = None
1593 self.response_headers = headers
1594 self.headers = None
1595 self.uri = None
1596 self.method = None
1597 self.body = None
1598 self.headers = None
1599
1600
1601 def request(self, uri,
1602 method='GET',
1603 body=None,
1604 headers=None,
1605 redirections=1,
1606 connection_type=None):
1607 self.uri = uri
1608 self.method = method
1609 self.body = body
1610 self.headers = headers
1611 return httplib2.Response(self.response_headers), self.data
1612
1613
1614class HttpMockSequence(object):
1615 """Mock of httplib2.Http
1616
1617 Mocks a sequence of calls to request returning different responses for each
1618 call. Create an instance initialized with the desired response headers
1619 and content and then use as if an httplib2.Http instance.
1620
1621 http = HttpMockSequence([
1622 ({'status': '401'}, ''),
1623 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1624 ({'status': '200'}, 'echo_request_headers'),
1625 ])
1626 resp, content = http.request("http://examples.com")
1627
1628 There are special values you can pass in for content to trigger
1629 behavours that are helpful in testing.
1630
1631 'echo_request_headers' means return the request headers in the response body
1632 'echo_request_headers_as_json' means return the request headers in
1633 the response body
1634 'echo_request_body' means return the request body in the response body
1635 'echo_request_uri' means return the request uri in the response body
1636 """
1637
1638 def __init__(self, iterable):
1639 """
1640 Args:
1641 iterable: iterable, a sequence of pairs of (headers, body)
1642 """
1643 self._iterable = iterable
1644 self.follow_redirects = True
1645
1646 def request(self, uri,
1647 method='GET',
1648 body=None,
1649 headers=None,
1650 redirections=1,
1651 connection_type=None):
1652 resp, content = self._iterable.pop(0)
1653 if content == 'echo_request_headers':
1654 content = headers
1655 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001656 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001657 elif content == 'echo_request_body':
1658 if hasattr(body, 'read'):
1659 content = body.read()
1660 else:
1661 content = body
1662 elif content == 'echo_request_uri':
1663 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001664 if isinstance(content, six.text_type):
1665 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001666 return httplib2.Response(resp), content
1667
1668
1669def set_user_agent(http, user_agent):
1670 """Set the user-agent on every request.
1671
1672 Args:
1673 http - An instance of httplib2.Http
1674 or something that acts like it.
1675 user_agent: string, the value for the user-agent header.
1676
1677 Returns:
1678 A modified instance of http that was passed in.
1679
1680 Example:
1681
1682 h = httplib2.Http()
1683 h = set_user_agent(h, "my-app-name/6.0")
1684
1685 Most of the time the user-agent will be set doing auth, this is for the rare
1686 cases where you are accessing an unauthenticated endpoint.
1687 """
1688 request_orig = http.request
1689
1690 # The closure that will replace 'httplib2.Http.request'.
1691 def new_request(uri, method='GET', body=None, headers=None,
1692 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1693 connection_type=None):
1694 """Modify the request headers to add the user-agent."""
1695 if headers is None:
1696 headers = {}
1697 if 'user-agent' in headers:
1698 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1699 else:
1700 headers['user-agent'] = user_agent
1701 resp, content = request_orig(uri, method, body, headers,
1702 redirections, connection_type)
1703 return resp, content
1704
1705 http.request = new_request
1706 return http
1707
1708
1709def tunnel_patch(http):
1710 """Tunnel PATCH requests over POST.
1711 Args:
1712 http - An instance of httplib2.Http
1713 or something that acts like it.
1714
1715 Returns:
1716 A modified instance of http that was passed in.
1717
1718 Example:
1719
1720 h = httplib2.Http()
1721 h = tunnel_patch(h, "my-app-name/6.0")
1722
1723 Useful if you are running on a platform that doesn't support PATCH.
1724 Apply this last if you are using OAuth 1.0, as changing the method
1725 will result in a different signature.
1726 """
1727 request_orig = http.request
1728
1729 # The closure that will replace 'httplib2.Http.request'.
1730 def new_request(uri, method='GET', body=None, headers=None,
1731 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1732 connection_type=None):
1733 """Modify the request headers to add the user-agent."""
1734 if headers is None:
1735 headers = {}
1736 if method == 'PATCH':
1737 if 'oauth_token' in headers.get('authorization', ''):
Emmett Butler09699152016-02-08 14:26:00 -08001738 LOGGER.warning(
John Asmuth864311d2014-04-24 15:46:08 -04001739 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1740 headers['x-http-method-override'] = "PATCH"
1741 method = 'POST'
1742 resp, content = request_orig(uri, method, body, headers,
1743 redirections, connection_type)
1744 return resp, content
1745
1746 http.request = new_request
1747 return http
Igor Maravić22435292017-01-19 22:28:22 +01001748
1749
1750def build_http():
1751 """Builds httplib2.Http object
1752
1753 Returns:
1754 A httplib2.Http object, which is used to make http requests, and which has timeout set by default.
1755 To override default timeout call
1756
1757 socket.setdefaulttimeout(timeout_in_sec)
1758
1759 before interacting with this method.
1760 """
1761 if socket.getdefaulttimeout() is not None:
1762 http_timeout = socket.getdefaulttimeout()
1763 else:
1764 http_timeout = DEFAULT_HTTP_TIMEOUT_SEC
1765 return httplib2.Http(timeout=http_timeout)