blob: 2245e8d709e1be4cb1356edd061bf37a3c44feaf [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
23from six.moves import range
John Asmuth864311d2014-04-24 15:46:08 -040024
25__author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
Pat Ferateed9affd2015-03-03 16:03:15 -080027from six import BytesIO, StringIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080028from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
Pat Ferateed9affd2015-03-03 16:03:15 -080029
John Asmuth864311d2014-04-24 15:46:08 -040030import base64
31import copy
32import gzip
33import httplib2
Craig Citro6ae34d72014-08-18 23:10:09 -070034import json
John Asmuth864311d2014-04-24 15:46:08 -040035import logging
John Asmuth864311d2014-04-24 15:46:08 -040036import mimetypes
37import os
38import random
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +010039import ssl
John Asmuth864311d2014-04-24 15:46:08 -040040import sys
41import time
John Asmuth864311d2014-04-24 15:46:08 -040042import uuid
43
44from email.generator import Generator
45from email.mime.multipart import MIMEMultipart
46from email.mime.nonmultipart import MIMENonMultipart
47from email.parser import FeedParser
Pat Ferateb240c172015-03-03 16:23:51 -080048
49from googleapiclient import mimeparse
50from googleapiclient.errors import BatchError
51from googleapiclient.errors import HttpError
52from googleapiclient.errors import InvalidChunkSizeError
53from googleapiclient.errors import ResumableUploadError
54from googleapiclient.errors import UnexpectedBodyError
55from googleapiclient.errors import UnexpectedMethodError
56from googleapiclient.model import JsonModel
John Asmuth864311d2014-04-24 15:46:08 -040057from oauth2client import util
John Asmuth864311d2014-04-24 15:46:08 -040058
59
60DEFAULT_CHUNK_SIZE = 512*1024
61
62MAX_URI_LENGTH = 2048
63
64
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +010065def _retry_request(http, num_retries, req_type, sleep, rand, uri, method, *args,
66 **kwargs):
67 """Retries an HTTP request multiple times while handling errors.
68
69 If after all retries the request still fails, last error is either returned as
70 return value (for HTTP 5xx errors) or thrown (for ssl.SSLError).
71
72 Args:
73 http: Http object to be used to execute request.
74 num_retries: Maximum number of retries.
75 req_type: Type of the request (used for logging retries).
76 sleep, rand: Functions to sleep for random time between retries.
77 uri: URI to be requested.
78 method: HTTP method to be used.
79 args, kwargs: Additional arguments passed to http.request.
80
81 Returns:
82 resp, content - Response from the http request (may be HTTP 5xx).
83 """
84 resp = None
85 for retry_num in range(num_retries + 1):
86 if retry_num > 0:
87 sleep(rand() * 2**retry_num)
88 logging.warning(
89 'Retry #%d for %s: %s %s%s' % (retry_num, req_type, method, uri,
90 ', following status: %d' % resp.status if resp else ''))
91
92 try:
93 resp, content = http.request(uri, method, *args, **kwargs)
94 except ssl.SSLError:
95 if retry_num == num_retries:
96 raise
97 else:
98 continue
99 if resp.status < 500:
100 break
101
102 return resp, content
103
104
John Asmuth864311d2014-04-24 15:46:08 -0400105class MediaUploadProgress(object):
106 """Status of a resumable upload."""
107
108 def __init__(self, resumable_progress, total_size):
109 """Constructor.
110
111 Args:
112 resumable_progress: int, bytes sent so far.
113 total_size: int, total bytes in complete upload, or None if the total
114 upload size isn't known ahead of time.
115 """
116 self.resumable_progress = resumable_progress
117 self.total_size = total_size
118
119 def progress(self):
120 """Percent of upload completed, as a float.
121
122 Returns:
123 the percentage complete as a float, returning 0.0 if the total size of
124 the upload is unknown.
125 """
126 if self.total_size is not None:
127 return float(self.resumable_progress) / float(self.total_size)
128 else:
129 return 0.0
130
131
132class MediaDownloadProgress(object):
133 """Status of a resumable download."""
134
135 def __init__(self, resumable_progress, total_size):
136 """Constructor.
137
138 Args:
139 resumable_progress: int, bytes received so far.
140 total_size: int, total bytes in complete download.
141 """
142 self.resumable_progress = resumable_progress
143 self.total_size = total_size
144
145 def progress(self):
146 """Percent of download completed, as a float.
147
148 Returns:
149 the percentage complete as a float, returning 0.0 if the total size of
150 the download is unknown.
151 """
152 if self.total_size is not None:
153 return float(self.resumable_progress) / float(self.total_size)
154 else:
155 return 0.0
156
157
158class MediaUpload(object):
159 """Describes a media object to upload.
160
161 Base class that defines the interface of MediaUpload subclasses.
162
163 Note that subclasses of MediaUpload may allow you to control the chunksize
164 when uploading a media object. It is important to keep the size of the chunk
165 as large as possible to keep the upload efficient. Other factors may influence
166 the size of the chunk you use, particularly if you are working in an
167 environment where individual HTTP requests may have a hardcoded time limit,
168 such as under certain classes of requests under Google App Engine.
169
170 Streams are io.Base compatible objects that support seek(). Some MediaUpload
171 subclasses support using streams directly to upload data. Support for
172 streaming may be indicated by a MediaUpload sub-class and if appropriate for a
173 platform that stream will be used for uploading the media object. The support
174 for streaming is indicated by has_stream() returning True. The stream() method
175 should return an io.Base object that supports seek(). On platforms where the
176 underlying httplib module supports streaming, for example Python 2.6 and
177 later, the stream will be passed into the http library which will result in
178 less memory being used and possibly faster uploads.
179
180 If you need to upload media that can't be uploaded using any of the existing
181 MediaUpload sub-class then you can sub-class MediaUpload for your particular
182 needs.
183 """
184
185 def chunksize(self):
186 """Chunk size for resumable uploads.
187
188 Returns:
189 Chunk size in bytes.
190 """
191 raise NotImplementedError()
192
193 def mimetype(self):
194 """Mime type of the body.
195
196 Returns:
197 Mime type.
198 """
199 return 'application/octet-stream'
200
201 def size(self):
202 """Size of upload.
203
204 Returns:
205 Size of the body, or None of the size is unknown.
206 """
207 return None
208
209 def resumable(self):
210 """Whether this upload is resumable.
211
212 Returns:
213 True if resumable upload or False.
214 """
215 return False
216
217 def getbytes(self, begin, end):
218 """Get bytes from the media.
219
220 Args:
221 begin: int, offset from beginning of file.
222 length: int, number of bytes to read, starting at begin.
223
224 Returns:
225 A string of bytes read. May be shorter than length if EOF was reached
226 first.
227 """
228 raise NotImplementedError()
229
230 def has_stream(self):
231 """Does the underlying upload support a streaming interface.
232
233 Streaming means it is an io.IOBase subclass that supports seek, i.e.
234 seekable() returns True.
235
236 Returns:
237 True if the call to stream() will return an instance of a seekable io.Base
238 subclass.
239 """
240 return False
241
242 def stream(self):
243 """A stream interface to the data being uploaded.
244
245 Returns:
246 The returned value is an io.IOBase subclass that supports seek, i.e.
247 seekable() returns True.
248 """
249 raise NotImplementedError()
250
251 @util.positional(1)
252 def _to_json(self, strip=None):
253 """Utility function for creating a JSON representation of a MediaUpload.
254
255 Args:
256 strip: array, An array of names of members to not include in the JSON.
257
258 Returns:
259 string, a JSON representation of this instance, suitable to pass to
260 from_json().
261 """
262 t = type(self)
263 d = copy.copy(self.__dict__)
264 if strip is not None:
265 for member in strip:
266 del d[member]
267 d['_class'] = t.__name__
268 d['_module'] = t.__module__
Craig Citro6ae34d72014-08-18 23:10:09 -0700269 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400270
271 def to_json(self):
272 """Create a JSON representation of an instance of MediaUpload.
273
274 Returns:
275 string, a JSON representation of this instance, suitable to pass to
276 from_json().
277 """
278 return self._to_json()
279
280 @classmethod
281 def new_from_json(cls, s):
282 """Utility class method to instantiate a MediaUpload subclass from a JSON
283 representation produced by to_json().
284
285 Args:
286 s: string, JSON from to_json().
287
288 Returns:
289 An instance of the subclass of MediaUpload that was serialized with
290 to_json().
291 """
Craig Citro6ae34d72014-08-18 23:10:09 -0700292 data = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400293 # Find and call the right classmethod from_json() to restore the object.
294 module = data['_module']
295 m = __import__(module, fromlist=module.split('.')[:-1])
296 kls = getattr(m, data['_class'])
297 from_json = getattr(kls, 'from_json')
298 return from_json(s)
299
300
301class MediaIoBaseUpload(MediaUpload):
302 """A MediaUpload for a io.Base objects.
303
304 Note that the Python file object is compatible with io.Base and can be used
305 with this class also.
306
Pat Ferateed9affd2015-03-03 16:03:15 -0800307 fh = BytesIO('...Some data to upload...')
John Asmuth864311d2014-04-24 15:46:08 -0400308 media = MediaIoBaseUpload(fh, mimetype='image/png',
309 chunksize=1024*1024, resumable=True)
310 farm.animals().insert(
311 id='cow',
312 name='cow.png',
313 media_body=media).execute()
314
315 Depending on the platform you are working on, you may pass -1 as the
316 chunksize, which indicates that the entire file should be uploaded in a single
317 request. If the underlying platform supports streams, such as Python 2.6 or
318 later, then this can be very efficient as it avoids multiple connections, and
319 also avoids loading the entire file into memory before sending it. Note that
320 Google App Engine has a 5MB limit on request size, so you should never set
321 your chunksize larger than 5MB, or to -1.
322 """
323
324 @util.positional(3)
325 def __init__(self, fd, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
326 resumable=False):
327 """Constructor.
328
329 Args:
330 fd: io.Base or file object, The source of the bytes to upload. MUST be
331 opened in blocking mode, do not use streams opened in non-blocking mode.
332 The given stream must be seekable, that is, it must be able to call
333 seek() on fd.
334 mimetype: string, Mime-type of the file.
335 chunksize: int, File will be uploaded in chunks of this many bytes. Only
336 used if resumable=True. Pass in a value of -1 if the file is to be
337 uploaded as a single chunk. Note that Google App Engine has a 5MB limit
338 on request size, so you should never set your chunksize larger than 5MB,
339 or to -1.
340 resumable: bool, True if this is a resumable upload. False means upload
341 in a single request.
342 """
343 super(MediaIoBaseUpload, self).__init__()
344 self._fd = fd
345 self._mimetype = mimetype
346 if not (chunksize == -1 or chunksize > 0):
347 raise InvalidChunkSizeError()
348 self._chunksize = chunksize
349 self._resumable = resumable
350
351 self._fd.seek(0, os.SEEK_END)
352 self._size = self._fd.tell()
353
354 def chunksize(self):
355 """Chunk size for resumable uploads.
356
357 Returns:
358 Chunk size in bytes.
359 """
360 return self._chunksize
361
362 def mimetype(self):
363 """Mime type of the body.
364
365 Returns:
366 Mime type.
367 """
368 return self._mimetype
369
370 def size(self):
371 """Size of upload.
372
373 Returns:
374 Size of the body, or None of the size is unknown.
375 """
376 return self._size
377
378 def resumable(self):
379 """Whether this upload is resumable.
380
381 Returns:
382 True if resumable upload or False.
383 """
384 return self._resumable
385
386 def getbytes(self, begin, length):
387 """Get bytes from the media.
388
389 Args:
390 begin: int, offset from beginning of file.
391 length: int, number of bytes to read, starting at begin.
392
393 Returns:
394 A string of bytes read. May be shorted than length if EOF was reached
395 first.
396 """
397 self._fd.seek(begin)
398 return self._fd.read(length)
399
400 def has_stream(self):
401 """Does the underlying upload support a streaming interface.
402
403 Streaming means it is an io.IOBase subclass that supports seek, i.e.
404 seekable() returns True.
405
406 Returns:
407 True if the call to stream() will return an instance of a seekable io.Base
408 subclass.
409 """
410 return True
411
412 def stream(self):
413 """A stream interface to the data being uploaded.
414
415 Returns:
416 The returned value is an io.IOBase subclass that supports seek, i.e.
417 seekable() returns True.
418 """
419 return self._fd
420
421 def to_json(self):
422 """This upload type is not serializable."""
423 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
424
425
426class MediaFileUpload(MediaIoBaseUpload):
427 """A MediaUpload for a file.
428
429 Construct a MediaFileUpload and pass as the media_body parameter of the
430 method. For example, if we had a service that allowed uploading images:
431
432
433 media = MediaFileUpload('cow.png', mimetype='image/png',
434 chunksize=1024*1024, resumable=True)
435 farm.animals().insert(
436 id='cow',
437 name='cow.png',
438 media_body=media).execute()
439
440 Depending on the platform you are working on, you may pass -1 as the
441 chunksize, which indicates that the entire file should be uploaded in a single
442 request. If the underlying platform supports streams, such as Python 2.6 or
443 later, then this can be very efficient as it avoids multiple connections, and
444 also avoids loading the entire file into memory before sending it. Note that
445 Google App Engine has a 5MB limit on request size, so you should never set
446 your chunksize larger than 5MB, or to -1.
447 """
448
449 @util.positional(2)
450 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE,
451 resumable=False):
452 """Constructor.
453
454 Args:
455 filename: string, Name of the file.
456 mimetype: string, Mime-type of the file. If None then a mime-type will be
457 guessed from the file extension.
458 chunksize: int, File will be uploaded in chunks of this many bytes. Only
459 used if resumable=True. Pass in a value of -1 if the file is to be
460 uploaded in a single chunk. Note that Google App Engine has a 5MB limit
461 on request size, so you should never set your chunksize larger than 5MB,
462 or to -1.
463 resumable: bool, True if this is a resumable upload. False means upload
464 in a single request.
465 """
466 self._filename = filename
467 fd = open(self._filename, 'rb')
468 if mimetype is None:
Nam T. Nguyendc136312015-12-01 10:18:56 -0800469 # No mimetype provided, make a guess.
470 mimetype, _ = mimetypes.guess_type(filename)
471 if mimetype is None:
472 # Guess failed, use octet-stream.
473 mimetype = 'application/octet-stream'
John Asmuth864311d2014-04-24 15:46:08 -0400474 super(MediaFileUpload, self).__init__(fd, mimetype, chunksize=chunksize,
475 resumable=resumable)
476
477 def to_json(self):
478 """Creating a JSON representation of an instance of MediaFileUpload.
479
480 Returns:
481 string, a JSON representation of this instance, suitable to pass to
482 from_json().
483 """
484 return self._to_json(strip=['_fd'])
485
486 @staticmethod
487 def from_json(s):
Craig Citro6ae34d72014-08-18 23:10:09 -0700488 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400489 return MediaFileUpload(d['_filename'], mimetype=d['_mimetype'],
490 chunksize=d['_chunksize'], resumable=d['_resumable'])
491
492
493class MediaInMemoryUpload(MediaIoBaseUpload):
494 """MediaUpload for a chunk of bytes.
495
496 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
497 the stream.
498 """
499
500 @util.positional(2)
501 def __init__(self, body, mimetype='application/octet-stream',
502 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
503 """Create a new MediaInMemoryUpload.
504
505 DEPRECATED: Use MediaIoBaseUpload with either io.TextIOBase or StringIO for
506 the stream.
507
508 Args:
509 body: string, Bytes of body content.
510 mimetype: string, Mime-type of the file or default of
511 'application/octet-stream'.
512 chunksize: int, File will be uploaded in chunks of this many bytes. Only
513 used if resumable=True.
514 resumable: bool, True if this is a resumable upload. False means upload
515 in a single request.
516 """
Pat Ferateed9affd2015-03-03 16:03:15 -0800517 fd = BytesIO(body)
John Asmuth864311d2014-04-24 15:46:08 -0400518 super(MediaInMemoryUpload, self).__init__(fd, mimetype, chunksize=chunksize,
519 resumable=resumable)
520
521
522class MediaIoBaseDownload(object):
523 """"Download media resources.
524
525 Note that the Python file object is compatible with io.Base and can be used
526 with this class also.
527
528
529 Example:
530 request = farms.animals().get_media(id='cow')
531 fh = io.FileIO('cow.png', mode='wb')
532 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
533
534 done = False
535 while done is False:
536 status, done = downloader.next_chunk()
537 if status:
538 print "Download %d%%." % int(status.progress() * 100)
539 print "Download Complete!"
540 """
541
542 @util.positional(3)
543 def __init__(self, fd, request, chunksize=DEFAULT_CHUNK_SIZE):
544 """Constructor.
545
546 Args:
547 fd: io.Base or file object, The stream in which to write the downloaded
548 bytes.
549 request: googleapiclient.http.HttpRequest, the media request to perform in
550 chunks.
551 chunksize: int, File will be downloaded in chunks of this many bytes.
552 """
553 self._fd = fd
554 self._request = request
555 self._uri = request.uri
556 self._chunksize = chunksize
557 self._progress = 0
558 self._total_size = None
559 self._done = False
560
561 # Stubs for testing.
562 self._sleep = time.sleep
563 self._rand = random.random
564
565 @util.positional(1)
566 def next_chunk(self, num_retries=0):
567 """Get the next chunk of the download.
568
569 Args:
570 num_retries: Integer, number of times to retry 500's with randomized
571 exponential backoff. If all retries fail, the raised HttpError
572 represents the last request. If zero (default), we attempt the
573 request only once.
574
575 Returns:
576 (status, done): (MediaDownloadStatus, boolean)
577 The value of 'done' will be True when the media has been fully
578 downloaded.
579
580 Raises:
581 googleapiclient.errors.HttpError if the response was not a 2xx.
582 httplib2.HttpLib2Error if a transport error has occured.
583 """
584 headers = {
585 'range': 'bytes=%d-%d' % (
586 self._progress, self._progress + self._chunksize)
587 }
588 http = self._request.http
589
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100590 resp, content = _retry_request(
591 http, num_retries, 'media download', self._sleep, self._rand, self._uri,
592 'GET', headers=headers)
John Asmuth864311d2014-04-24 15:46:08 -0400593
594 if resp.status in [200, 206]:
595 if 'content-location' in resp and resp['content-location'] != self._uri:
596 self._uri = resp['content-location']
597 self._progress += len(content)
598 self._fd.write(content)
599
600 if 'content-range' in resp:
601 content_range = resp['content-range']
602 length = content_range.rsplit('/', 1)[1]
603 self._total_size = int(length)
jackac8df212015-02-17 12:16:19 -0800604 elif 'content-length' in resp:
jack77c63c92015-02-10 12:11:00 -0800605 self._total_size = int(resp['content-length'])
John Asmuth864311d2014-04-24 15:46:08 -0400606
607 if self._progress == self._total_size:
608 self._done = True
609 return MediaDownloadProgress(self._progress, self._total_size), self._done
610 else:
611 raise HttpError(resp, content, uri=self._uri)
612
613
614class _StreamSlice(object):
615 """Truncated stream.
616
617 Takes a stream and presents a stream that is a slice of the original stream.
618 This is used when uploading media in chunks. In later versions of Python a
619 stream can be passed to httplib in place of the string of data to send. The
620 problem is that httplib just blindly reads to the end of the stream. This
621 wrapper presents a virtual stream that only reads to the end of the chunk.
622 """
623
624 def __init__(self, stream, begin, chunksize):
625 """Constructor.
626
627 Args:
628 stream: (io.Base, file object), the stream to wrap.
629 begin: int, the seek position the chunk begins at.
630 chunksize: int, the size of the chunk.
631 """
632 self._stream = stream
633 self._begin = begin
634 self._chunksize = chunksize
635 self._stream.seek(begin)
636
637 def read(self, n=-1):
638 """Read n bytes.
639
640 Args:
641 n, int, the number of bytes to read.
642
643 Returns:
644 A string of length 'n', or less if EOF is reached.
645 """
646 # The data left available to read sits in [cur, end)
647 cur = self._stream.tell()
648 end = self._begin + self._chunksize
649 if n == -1 or cur + n > end:
650 n = end - cur
651 return self._stream.read(n)
652
653
654class HttpRequest(object):
655 """Encapsulates a single HTTP request."""
656
657 @util.positional(4)
658 def __init__(self, http, postproc, uri,
659 method='GET',
660 body=None,
661 headers=None,
662 methodId=None,
663 resumable=None):
664 """Constructor for an HttpRequest.
665
666 Args:
667 http: httplib2.Http, the transport object to use to make a request
668 postproc: callable, called on the HTTP response and content to transform
669 it into a data object before returning, or raising an exception
670 on an error.
671 uri: string, the absolute URI to send the request to
672 method: string, the HTTP method to use
673 body: string, the request body of the HTTP request,
674 headers: dict, the HTTP request headers
675 methodId: string, a unique identifier for the API method being called.
676 resumable: MediaUpload, None if this is not a resumbale request.
677 """
678 self.uri = uri
679 self.method = method
680 self.body = body
681 self.headers = headers or {}
682 self.methodId = methodId
683 self.http = http
684 self.postproc = postproc
685 self.resumable = resumable
686 self.response_callbacks = []
687 self._in_error_state = False
688
689 # Pull the multipart boundary out of the content-type header.
690 major, minor, params = mimeparse.parse_mime_type(
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100691 self.headers.get('content-type', 'application/json'))
John Asmuth864311d2014-04-24 15:46:08 -0400692
693 # The size of the non-media part of the request.
694 self.body_size = len(self.body or '')
695
696 # The resumable URI to send chunks to.
697 self.resumable_uri = None
698
699 # The bytes that have been uploaded.
700 self.resumable_progress = 0
701
702 # Stubs for testing.
703 self._rand = random.random
704 self._sleep = time.sleep
705
706 @util.positional(1)
707 def execute(self, http=None, num_retries=0):
708 """Execute the request.
709
710 Args:
711 http: httplib2.Http, an http object to be used in place of the
712 one the HttpRequest request object was constructed with.
713 num_retries: Integer, number of times to retry 500's with randomized
714 exponential backoff. If all retries fail, the raised HttpError
715 represents the last request. If zero (default), we attempt the
716 request only once.
717
718 Returns:
719 A deserialized object model of the response body as determined
720 by the postproc.
721
722 Raises:
723 googleapiclient.errors.HttpError if the response was not a 2xx.
724 httplib2.HttpLib2Error if a transport error has occured.
725 """
726 if http is None:
727 http = self.http
728
729 if self.resumable:
730 body = None
731 while body is None:
732 _, body = self.next_chunk(http=http, num_retries=num_retries)
733 return body
734
735 # Non-resumable case.
736
737 if 'content-length' not in self.headers:
738 self.headers['content-length'] = str(self.body_size)
739 # If the request URI is too long then turn it into a POST request.
740 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
741 self.method = 'POST'
742 self.headers['x-http-method-override'] = 'GET'
743 self.headers['content-type'] = 'application/x-www-form-urlencoded'
Pat Ferated5b61bd2015-03-03 16:04:11 -0800744 parsed = urlparse(self.uri)
745 self.uri = urlunparse(
John Asmuth864311d2014-04-24 15:46:08 -0400746 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
747 None)
748 )
749 self.body = parsed.query
750 self.headers['content-length'] = str(len(self.body))
751
752 # Handle retries for server-side errors.
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100753 resp, content = _retry_request(
754 http, num_retries, 'request', self._sleep, self._rand, str(self.uri),
755 method=str(self.method), body=self.body, headers=self.headers)
John Asmuth864311d2014-04-24 15:46:08 -0400756
757 for callback in self.response_callbacks:
758 callback(resp)
759 if resp.status >= 300:
760 raise HttpError(resp, content, uri=self.uri)
761 return self.postproc(resp, content)
762
763 @util.positional(2)
764 def add_response_callback(self, cb):
765 """add_response_headers_callback
766
767 Args:
768 cb: Callback to be called on receiving the response headers, of signature:
769
770 def cb(resp):
771 # Where resp is an instance of httplib2.Response
772 """
773 self.response_callbacks.append(cb)
774
775 @util.positional(1)
776 def next_chunk(self, http=None, num_retries=0):
777 """Execute the next step of a resumable upload.
778
779 Can only be used if the method being executed supports media uploads and
780 the MediaUpload object passed in was flagged as using resumable upload.
781
782 Example:
783
784 media = MediaFileUpload('cow.png', mimetype='image/png',
785 chunksize=1000, resumable=True)
786 request = farm.animals().insert(
787 id='cow',
788 name='cow.png',
789 media_body=media)
790
791 response = None
792 while response is None:
793 status, response = request.next_chunk()
794 if status:
795 print "Upload %d%% complete." % int(status.progress() * 100)
796
797
798 Args:
799 http: httplib2.Http, an http object to be used in place of the
800 one the HttpRequest request object was constructed with.
801 num_retries: Integer, number of times to retry 500's with randomized
802 exponential backoff. If all retries fail, the raised HttpError
803 represents the last request. If zero (default), we attempt the
804 request only once.
805
806 Returns:
807 (status, body): (ResumableMediaStatus, object)
808 The body will be None until the resumable media is fully uploaded.
809
810 Raises:
811 googleapiclient.errors.HttpError if the response was not a 2xx.
812 httplib2.HttpLib2Error if a transport error has occured.
813 """
814 if http is None:
815 http = self.http
816
817 if self.resumable.size() is None:
818 size = '*'
819 else:
820 size = str(self.resumable.size())
821
822 if self.resumable_uri is None:
823 start_headers = copy.copy(self.headers)
824 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
825 if size != '*':
826 start_headers['X-Upload-Content-Length'] = size
827 start_headers['content-length'] = str(self.body_size)
828
Sergiy Byelozyorov703c92c2015-12-21 23:27:48 +0100829 resp, content = _retry_request(
830 http, num_retries, 'resumable URI request', self._sleep, self._rand,
831 self.uri, method=self.method, body=self.body, headers=start_headers)
John Asmuth864311d2014-04-24 15:46:08 -0400832
833 if resp.status == 200 and 'location' in resp:
834 self.resumable_uri = resp['location']
835 else:
836 raise ResumableUploadError(resp, content)
837 elif self._in_error_state:
838 # If we are in an error state then query the server for current state of
839 # the upload by sending an empty PUT and reading the 'range' header in
840 # the response.
841 headers = {
842 'Content-Range': 'bytes */%s' % size,
843 'content-length': '0'
844 }
845 resp, content = http.request(self.resumable_uri, 'PUT',
846 headers=headers)
847 status, body = self._process_response(resp, content)
848 if body:
849 # The upload was complete.
850 return (status, body)
851
e00Efafe8582015-10-10 18:19:37 +0200852 if self.resumable.has_stream():
John Asmuth864311d2014-04-24 15:46:08 -0400853 data = self.resumable.stream()
854 if self.resumable.chunksize() == -1:
855 data.seek(self.resumable_progress)
856 chunk_end = self.resumable.size() - self.resumable_progress - 1
857 else:
858 # Doing chunking with a stream, so wrap a slice of the stream.
859 data = _StreamSlice(data, self.resumable_progress,
860 self.resumable.chunksize())
861 chunk_end = min(
862 self.resumable_progress + self.resumable.chunksize() - 1,
863 self.resumable.size() - 1)
864 else:
865 data = self.resumable.getbytes(
866 self.resumable_progress, self.resumable.chunksize())
867
868 # A short read implies that we are at EOF, so finish the upload.
869 if len(data) < self.resumable.chunksize():
870 size = str(self.resumable_progress + len(data))
871
872 chunk_end = self.resumable_progress + len(data) - 1
873
874 headers = {
875 'Content-Range': 'bytes %d-%d/%s' % (
876 self.resumable_progress, chunk_end, size),
877 # Must set the content-length header here because httplib can't
878 # calculate the size when working with _StreamSlice.
879 'Content-Length': str(chunk_end - self.resumable_progress + 1)
880 }
881
INADA Naokie4ea1a92015-03-04 03:45:42 +0900882 for retry_num in range(num_retries + 1):
John Asmuth864311d2014-04-24 15:46:08 -0400883 if retry_num > 0:
884 self._sleep(self._rand() * 2**retry_num)
885 logging.warning(
886 'Retry #%d for media upload: %s %s, following status: %d'
887 % (retry_num, self.method, self.uri, resp.status))
888
889 try:
890 resp, content = http.request(self.resumable_uri, method='PUT',
891 body=data,
892 headers=headers)
893 except:
894 self._in_error_state = True
895 raise
896 if resp.status < 500:
897 break
898
899 return self._process_response(resp, content)
900
901 def _process_response(self, resp, content):
902 """Process the response from a single chunk upload.
903
904 Args:
905 resp: httplib2.Response, the response object.
906 content: string, the content of the response.
907
908 Returns:
909 (status, body): (ResumableMediaStatus, object)
910 The body will be None until the resumable media is fully uploaded.
911
912 Raises:
913 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
914 """
915 if resp.status in [200, 201]:
916 self._in_error_state = False
917 return None, self.postproc(resp, content)
918 elif resp.status == 308:
919 self._in_error_state = False
920 # A "308 Resume Incomplete" indicates we are not done.
921 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
922 if 'location' in resp:
923 self.resumable_uri = resp['location']
924 else:
925 self._in_error_state = True
926 raise HttpError(resp, content, uri=self.uri)
927
928 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
929 None)
930
931 def to_json(self):
932 """Returns a JSON representation of the HttpRequest."""
933 d = copy.copy(self.__dict__)
934 if d['resumable'] is not None:
935 d['resumable'] = self.resumable.to_json()
936 del d['http']
937 del d['postproc']
938 del d['_sleep']
939 del d['_rand']
940
Craig Citro6ae34d72014-08-18 23:10:09 -0700941 return json.dumps(d)
John Asmuth864311d2014-04-24 15:46:08 -0400942
943 @staticmethod
944 def from_json(s, http, postproc):
945 """Returns an HttpRequest populated with info from a JSON object."""
Craig Citro6ae34d72014-08-18 23:10:09 -0700946 d = json.loads(s)
John Asmuth864311d2014-04-24 15:46:08 -0400947 if d['resumable'] is not None:
948 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
949 return HttpRequest(
950 http,
951 postproc,
952 uri=d['uri'],
953 method=d['method'],
954 body=d['body'],
955 headers=d['headers'],
956 methodId=d['methodId'],
957 resumable=d['resumable'])
958
959
960class BatchHttpRequest(object):
961 """Batches multiple HttpRequest objects into a single HTTP request.
962
963 Example:
964 from googleapiclient.http import BatchHttpRequest
965
966 def list_animals(request_id, response, exception):
967 \"\"\"Do something with the animals list response.\"\"\"
968 if exception is not None:
969 # Do something with the exception.
970 pass
971 else:
972 # Do something with the response.
973 pass
974
975 def list_farmers(request_id, response, exception):
976 \"\"\"Do something with the farmers list response.\"\"\"
977 if exception is not None:
978 # Do something with the exception.
979 pass
980 else:
981 # Do something with the response.
982 pass
983
984 service = build('farm', 'v2')
985
986 batch = BatchHttpRequest()
987
988 batch.add(service.animals().list(), list_animals)
989 batch.add(service.farmers().list(), list_farmers)
990 batch.execute(http=http)
991 """
992
993 @util.positional(1)
994 def __init__(self, callback=None, batch_uri=None):
995 """Constructor for a BatchHttpRequest.
996
997 Args:
998 callback: callable, A callback to be called for each response, of the
999 form callback(id, response, exception). The first parameter is the
1000 request id, and the second is the deserialized response object. The
1001 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1002 occurred while processing the request, or None if no error occurred.
1003 batch_uri: string, URI to send batch requests to.
1004 """
1005 if batch_uri is None:
1006 batch_uri = 'https://www.googleapis.com/batch'
1007 self._batch_uri = batch_uri
1008
1009 # Global callback to be called for each individual response in the batch.
1010 self._callback = callback
1011
1012 # A map from id to request.
1013 self._requests = {}
1014
1015 # A map from id to callback.
1016 self._callbacks = {}
1017
1018 # List of request ids, in the order in which they were added.
1019 self._order = []
1020
1021 # The last auto generated id.
1022 self._last_auto_id = 0
1023
1024 # Unique ID on which to base the Content-ID headers.
1025 self._base_id = None
1026
1027 # A map from request id to (httplib2.Response, content) response pairs
1028 self._responses = {}
1029
1030 # A map of id(Credentials) that have been refreshed.
1031 self._refreshed_credentials = {}
1032
1033 def _refresh_and_apply_credentials(self, request, http):
1034 """Refresh the credentials and apply to the request.
1035
1036 Args:
1037 request: HttpRequest, the request.
1038 http: httplib2.Http, the global http object for the batch.
1039 """
1040 # For the credentials to refresh, but only once per refresh_token
1041 # If there is no http per the request then refresh the http passed in
1042 # via execute()
1043 creds = None
1044 if request.http is not None and hasattr(request.http.request,
1045 'credentials'):
1046 creds = request.http.request.credentials
1047 elif http is not None and hasattr(http.request, 'credentials'):
1048 creds = http.request.credentials
1049 if creds is not None:
1050 if id(creds) not in self._refreshed_credentials:
1051 creds.refresh(http)
1052 self._refreshed_credentials[id(creds)] = 1
1053
1054 # Only apply the credentials if we are using the http object passed in,
1055 # otherwise apply() will get called during _serialize_request().
1056 if request.http is None or not hasattr(request.http.request,
1057 'credentials'):
1058 creds.apply(request.headers)
1059
1060 def _id_to_header(self, id_):
1061 """Convert an id to a Content-ID header value.
1062
1063 Args:
1064 id_: string, identifier of individual request.
1065
1066 Returns:
1067 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1068 the value because Content-ID headers are supposed to be universally
1069 unique.
1070 """
1071 if self._base_id is None:
1072 self._base_id = uuid.uuid4()
1073
Pat Ferated5b61bd2015-03-03 16:04:11 -08001074 return '<%s+%s>' % (self._base_id, quote(id_))
John Asmuth864311d2014-04-24 15:46:08 -04001075
1076 def _header_to_id(self, header):
1077 """Convert a Content-ID header value to an id.
1078
1079 Presumes the Content-ID header conforms to the format that _id_to_header()
1080 returns.
1081
1082 Args:
1083 header: string, Content-ID header value.
1084
1085 Returns:
1086 The extracted id value.
1087
1088 Raises:
1089 BatchError if the header is not in the expected format.
1090 """
1091 if header[0] != '<' or header[-1] != '>':
1092 raise BatchError("Invalid value for Content-ID: %s" % header)
1093 if '+' not in header:
1094 raise BatchError("Invalid value for Content-ID: %s" % header)
1095 base, id_ = header[1:-1].rsplit('+', 1)
1096
Pat Ferated5b61bd2015-03-03 16:04:11 -08001097 return unquote(id_)
John Asmuth864311d2014-04-24 15:46:08 -04001098
1099 def _serialize_request(self, request):
1100 """Convert an HttpRequest object into a string.
1101
1102 Args:
1103 request: HttpRequest, the request to serialize.
1104
1105 Returns:
1106 The request as a string in application/http format.
1107 """
1108 # Construct status line
Pat Ferated5b61bd2015-03-03 16:04:11 -08001109 parsed = urlparse(request.uri)
1110 request_line = urlunparse(
Pat Feratec9abbbd2015-03-03 18:00:38 -08001111 ('', '', parsed.path, parsed.params, parsed.query, '')
John Asmuth864311d2014-04-24 15:46:08 -04001112 )
1113 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1114 major, minor = request.headers.get('content-type', 'application/json').split('/')
1115 msg = MIMENonMultipart(major, minor)
1116 headers = request.headers.copy()
1117
1118 if request.http is not None and hasattr(request.http.request,
1119 'credentials'):
1120 request.http.request.credentials.apply(headers)
1121
1122 # MIMENonMultipart adds its own Content-Type header.
1123 if 'content-type' in headers:
1124 del headers['content-type']
1125
INADA Naokie4ea1a92015-03-04 03:45:42 +09001126 for key, value in six.iteritems(headers):
John Asmuth864311d2014-04-24 15:46:08 -04001127 msg[key] = value
1128 msg['Host'] = parsed.netloc
1129 msg.set_unixfrom(None)
1130
1131 if request.body is not None:
1132 msg.set_payload(request.body)
1133 msg['content-length'] = str(len(request.body))
1134
1135 # Serialize the mime message.
Pat Ferateed9affd2015-03-03 16:03:15 -08001136 fp = StringIO()
John Asmuth864311d2014-04-24 15:46:08 -04001137 # maxheaderlen=0 means don't line wrap headers.
1138 g = Generator(fp, maxheaderlen=0)
1139 g.flatten(msg, unixfrom=False)
1140 body = fp.getvalue()
1141
Pat Feratec9abbbd2015-03-03 18:00:38 -08001142 return status_line + body
John Asmuth864311d2014-04-24 15:46:08 -04001143
1144 def _deserialize_response(self, payload):
1145 """Convert string into httplib2 response and content.
1146
1147 Args:
1148 payload: string, headers and body as a string.
1149
1150 Returns:
1151 A pair (resp, content), such as would be returned from httplib2.request.
1152 """
1153 # Strip off the status line
1154 status_line, payload = payload.split('\n', 1)
1155 protocol, status, reason = status_line.split(' ', 2)
1156
1157 # Parse the rest of the response
1158 parser = FeedParser()
1159 parser.feed(payload)
1160 msg = parser.close()
1161 msg['status'] = status
1162
1163 # Create httplib2.Response from the parsed headers.
1164 resp = httplib2.Response(msg)
1165 resp.reason = reason
1166 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1167
1168 content = payload.split('\r\n\r\n', 1)[1]
1169
1170 return resp, content
1171
1172 def _new_id(self):
1173 """Create a new id.
1174
1175 Auto incrementing number that avoids conflicts with ids already used.
1176
1177 Returns:
1178 string, a new unique id.
1179 """
1180 self._last_auto_id += 1
1181 while str(self._last_auto_id) in self._requests:
1182 self._last_auto_id += 1
1183 return str(self._last_auto_id)
1184
1185 @util.positional(2)
1186 def add(self, request, callback=None, request_id=None):
1187 """Add a new request.
1188
1189 Every callback added will be paired with a unique id, the request_id. That
1190 unique id will be passed back to the callback when the response comes back
1191 from the server. The default behavior is to have the library generate it's
1192 own unique id. If the caller passes in a request_id then they must ensure
1193 uniqueness for each request_id, and if they are not an exception is
1194 raised. Callers should either supply all request_ids or nevery supply a
1195 request id, to avoid such an error.
1196
1197 Args:
1198 request: HttpRequest, Request to add to the batch.
1199 callback: callable, A callback to be called for this response, of the
1200 form callback(id, response, exception). The first parameter is the
1201 request id, and the second is the deserialized response object. The
1202 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1203 occurred while processing the request, or None if no errors occurred.
1204 request_id: string, A unique id for the request. The id will be passed to
1205 the callback with the response.
1206
1207 Returns:
1208 None
1209
1210 Raises:
1211 BatchError if a media request is added to a batch.
1212 KeyError is the request_id is not unique.
1213 """
1214 if request_id is None:
1215 request_id = self._new_id()
1216 if request.resumable is not None:
1217 raise BatchError("Media requests cannot be used in a batch request.")
1218 if request_id in self._requests:
1219 raise KeyError("A request with this ID already exists: %s" % request_id)
1220 self._requests[request_id] = request
1221 self._callbacks[request_id] = callback
1222 self._order.append(request_id)
1223
1224 def _execute(self, http, order, requests):
1225 """Serialize batch request, send to server, process response.
1226
1227 Args:
1228 http: httplib2.Http, an http object to be used to make the request with.
1229 order: list, list of request ids in the order they were added to the
1230 batch.
1231 request: list, list of request objects to send.
1232
1233 Raises:
1234 httplib2.HttpLib2Error if a transport error has occured.
1235 googleapiclient.errors.BatchError if the response is the wrong format.
1236 """
1237 message = MIMEMultipart('mixed')
1238 # Message should not write out it's own headers.
1239 setattr(message, '_write_headers', lambda self: None)
1240
1241 # Add all the individual requests.
1242 for request_id in order:
1243 request = requests[request_id]
1244
1245 msg = MIMENonMultipart('application', 'http')
1246 msg['Content-Transfer-Encoding'] = 'binary'
1247 msg['Content-ID'] = self._id_to_header(request_id)
1248
1249 body = self._serialize_request(request)
1250 msg.set_payload(body)
1251 message.attach(msg)
1252
Craig Citro72389b72014-07-15 17:12:50 -07001253 # encode the body: note that we can't use `as_string`, because
1254 # it plays games with `From ` lines.
Pat Ferateed9affd2015-03-03 16:03:15 -08001255 fp = StringIO()
Craig Citro72389b72014-07-15 17:12:50 -07001256 g = Generator(fp, mangle_from_=False)
1257 g.flatten(message, unixfrom=False)
1258 body = fp.getvalue()
John Asmuth864311d2014-04-24 15:46:08 -04001259
1260 headers = {}
1261 headers['content-type'] = ('multipart/mixed; '
1262 'boundary="%s"') % message.get_boundary()
1263
1264 resp, content = http.request(self._batch_uri, method='POST', body=body,
1265 headers=headers)
1266
1267 if resp.status >= 300:
1268 raise HttpError(resp, content, uri=self._batch_uri)
1269
John Asmuth864311d2014-04-24 15:46:08 -04001270 # Prepend with a content-type header so FeedParser can handle it.
1271 header = 'content-type: %s\r\n\r\n' % resp['content-type']
INADA Naoki09157612015-03-25 01:51:03 +09001272 # PY3's FeedParser only accepts unicode. So we should decode content
1273 # here, and encode each payload again.
1274 if six.PY3:
1275 content = content.decode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001276 for_parser = header + content
1277
1278 parser = FeedParser()
1279 parser.feed(for_parser)
1280 mime_response = parser.close()
1281
1282 if not mime_response.is_multipart():
1283 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1284 content=content)
1285
1286 for part in mime_response.get_payload():
1287 request_id = self._header_to_id(part['Content-ID'])
1288 response, content = self._deserialize_response(part.get_payload())
INADA Naoki09157612015-03-25 01:51:03 +09001289 # We encode content here to emulate normal http response.
1290 if isinstance(content, six.text_type):
1291 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001292 self._responses[request_id] = (response, content)
1293
1294 @util.positional(1)
1295 def execute(self, http=None):
1296 """Execute all the requests as a single batched HTTP request.
1297
1298 Args:
1299 http: httplib2.Http, an http object to be used in place of the one the
1300 HttpRequest request object was constructed with. If one isn't supplied
1301 then use a http object from the requests in this batch.
1302
1303 Returns:
1304 None
1305
1306 Raises:
1307 httplib2.HttpLib2Error if a transport error has occured.
1308 googleapiclient.errors.BatchError if the response is the wrong format.
1309 """
Mohamed Zenadi1b5350d2015-07-30 11:52:39 +02001310 # If we have no requests return
1311 if len(self._order) == 0:
1312 return None
John Asmuth864311d2014-04-24 15:46:08 -04001313
1314 # If http is not supplied use the first valid one given in the requests.
1315 if http is None:
1316 for request_id in self._order:
1317 request = self._requests[request_id]
1318 if request is not None:
1319 http = request.http
1320 break
1321
1322 if http is None:
1323 raise ValueError("Missing a valid http object.")
1324
1325 self._execute(http, self._order, self._requests)
1326
1327 # Loop over all the requests and check for 401s. For each 401 request the
1328 # credentials should be refreshed and then sent again in a separate batch.
1329 redo_requests = {}
1330 redo_order = []
1331
1332 for request_id in self._order:
1333 resp, content = self._responses[request_id]
1334 if resp['status'] == '401':
1335 redo_order.append(request_id)
1336 request = self._requests[request_id]
1337 self._refresh_and_apply_credentials(request, http)
1338 redo_requests[request_id] = request
1339
1340 if redo_requests:
1341 self._execute(http, redo_order, redo_requests)
1342
1343 # Now process all callbacks that are erroring, and raise an exception for
1344 # ones that return a non-2xx response? Or add extra parameter to callback
1345 # that contains an HttpError?
1346
1347 for request_id in self._order:
1348 resp, content = self._responses[request_id]
1349
1350 request = self._requests[request_id]
1351 callback = self._callbacks[request_id]
1352
1353 response = None
1354 exception = None
1355 try:
1356 if resp.status >= 300:
1357 raise HttpError(resp, content, uri=request.uri)
1358 response = request.postproc(resp, content)
INADA Naokic1505df2014-08-20 15:19:53 +09001359 except HttpError as e:
John Asmuth864311d2014-04-24 15:46:08 -04001360 exception = e
1361
1362 if callback is not None:
1363 callback(request_id, response, exception)
1364 if self._callback is not None:
1365 self._callback(request_id, response, exception)
1366
1367
1368class HttpRequestMock(object):
1369 """Mock of HttpRequest.
1370
1371 Do not construct directly, instead use RequestMockBuilder.
1372 """
1373
1374 def __init__(self, resp, content, postproc):
1375 """Constructor for HttpRequestMock
1376
1377 Args:
1378 resp: httplib2.Response, the response to emulate coming from the request
1379 content: string, the response body
1380 postproc: callable, the post processing function usually supplied by
1381 the model class. See model.JsonModel.response() as an example.
1382 """
1383 self.resp = resp
1384 self.content = content
1385 self.postproc = postproc
1386 if resp is None:
1387 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1388 if 'reason' in self.resp:
1389 self.resp.reason = self.resp['reason']
1390
1391 def execute(self, http=None):
1392 """Execute the request.
1393
1394 Same behavior as HttpRequest.execute(), but the response is
1395 mocked and not really from an HTTP request/response.
1396 """
1397 return self.postproc(self.resp, self.content)
1398
1399
1400class RequestMockBuilder(object):
1401 """A simple mock of HttpRequest
1402
1403 Pass in a dictionary to the constructor that maps request methodIds to
1404 tuples of (httplib2.Response, content, opt_expected_body) that should be
1405 returned when that method is called. None may also be passed in for the
1406 httplib2.Response, in which case a 200 OK response will be generated.
1407 If an opt_expected_body (str or dict) is provided, it will be compared to
1408 the body and UnexpectedBodyError will be raised on inequality.
1409
1410 Example:
1411 response = '{"data": {"id": "tag:google.c...'
1412 requestBuilder = RequestMockBuilder(
1413 {
1414 'plus.activities.get': (None, response),
1415 }
1416 )
1417 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1418
1419 Methods that you do not supply a response for will return a
1420 200 OK with an empty string as the response content or raise an excpetion
1421 if check_unexpected is set to True. The methodId is taken from the rpcName
1422 in the discovery document.
1423
1424 For more details see the project wiki.
1425 """
1426
1427 def __init__(self, responses, check_unexpected=False):
1428 """Constructor for RequestMockBuilder
1429
1430 The constructed object should be a callable object
1431 that can replace the class HttpResponse.
1432
1433 responses - A dictionary that maps methodIds into tuples
1434 of (httplib2.Response, content). The methodId
1435 comes from the 'rpcName' field in the discovery
1436 document.
1437 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1438 should be raised on unsupplied method.
1439 """
1440 self.responses = responses
1441 self.check_unexpected = check_unexpected
1442
1443 def __call__(self, http, postproc, uri, method='GET', body=None,
1444 headers=None, methodId=None, resumable=None):
1445 """Implements the callable interface that discovery.build() expects
1446 of requestBuilder, which is to build an object compatible with
1447 HttpRequest.execute(). See that method for the description of the
1448 parameters and the expected response.
1449 """
1450 if methodId in self.responses:
1451 response = self.responses[methodId]
1452 resp, content = response[:2]
1453 if len(response) > 2:
1454 # Test the body against the supplied expected_body.
1455 expected_body = response[2]
1456 if bool(expected_body) != bool(body):
1457 # Not expecting a body and provided one
1458 # or expecting a body and not provided one.
1459 raise UnexpectedBodyError(expected_body, body)
1460 if isinstance(expected_body, str):
Craig Citro6ae34d72014-08-18 23:10:09 -07001461 expected_body = json.loads(expected_body)
1462 body = json.loads(body)
John Asmuth864311d2014-04-24 15:46:08 -04001463 if body != expected_body:
1464 raise UnexpectedBodyError(expected_body, body)
1465 return HttpRequestMock(resp, content, postproc)
1466 elif self.check_unexpected:
1467 raise UnexpectedMethodError(methodId=methodId)
1468 else:
1469 model = JsonModel(False)
1470 return HttpRequestMock(None, '{}', model.response)
1471
1472
1473class HttpMock(object):
1474 """Mock of httplib2.Http"""
1475
1476 def __init__(self, filename=None, headers=None):
1477 """
1478 Args:
1479 filename: string, absolute filename to read response from
1480 headers: dict, header to return with response
1481 """
1482 if headers is None:
Craig Gurnik8e55b762015-01-20 15:00:10 -05001483 headers = {'status': '200'}
John Asmuth864311d2014-04-24 15:46:08 -04001484 if filename:
Alan Briolat26b01002015-08-14 00:13:57 +01001485 f = open(filename, 'rb')
John Asmuth864311d2014-04-24 15:46:08 -04001486 self.data = f.read()
1487 f.close()
1488 else:
1489 self.data = None
1490 self.response_headers = headers
1491 self.headers = None
1492 self.uri = None
1493 self.method = None
1494 self.body = None
1495 self.headers = None
1496
1497
1498 def request(self, uri,
1499 method='GET',
1500 body=None,
1501 headers=None,
1502 redirections=1,
1503 connection_type=None):
1504 self.uri = uri
1505 self.method = method
1506 self.body = body
1507 self.headers = headers
1508 return httplib2.Response(self.response_headers), self.data
1509
1510
1511class HttpMockSequence(object):
1512 """Mock of httplib2.Http
1513
1514 Mocks a sequence of calls to request returning different responses for each
1515 call. Create an instance initialized with the desired response headers
1516 and content and then use as if an httplib2.Http instance.
1517
1518 http = HttpMockSequence([
1519 ({'status': '401'}, ''),
1520 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1521 ({'status': '200'}, 'echo_request_headers'),
1522 ])
1523 resp, content = http.request("http://examples.com")
1524
1525 There are special values you can pass in for content to trigger
1526 behavours that are helpful in testing.
1527
1528 'echo_request_headers' means return the request headers in the response body
1529 'echo_request_headers_as_json' means return the request headers in
1530 the response body
1531 'echo_request_body' means return the request body in the response body
1532 'echo_request_uri' means return the request uri in the response body
1533 """
1534
1535 def __init__(self, iterable):
1536 """
1537 Args:
1538 iterable: iterable, a sequence of pairs of (headers, body)
1539 """
1540 self._iterable = iterable
1541 self.follow_redirects = True
1542
1543 def request(self, uri,
1544 method='GET',
1545 body=None,
1546 headers=None,
1547 redirections=1,
1548 connection_type=None):
1549 resp, content = self._iterable.pop(0)
1550 if content == 'echo_request_headers':
1551 content = headers
1552 elif content == 'echo_request_headers_as_json':
Craig Citro6ae34d72014-08-18 23:10:09 -07001553 content = json.dumps(headers)
John Asmuth864311d2014-04-24 15:46:08 -04001554 elif content == 'echo_request_body':
1555 if hasattr(body, 'read'):
1556 content = body.read()
1557 else:
1558 content = body
1559 elif content == 'echo_request_uri':
1560 content = uri
INADA Naoki09157612015-03-25 01:51:03 +09001561 if isinstance(content, six.text_type):
1562 content = content.encode('utf-8')
John Asmuth864311d2014-04-24 15:46:08 -04001563 return httplib2.Response(resp), content
1564
1565
1566def set_user_agent(http, user_agent):
1567 """Set the user-agent on every request.
1568
1569 Args:
1570 http - An instance of httplib2.Http
1571 or something that acts like it.
1572 user_agent: string, the value for the user-agent header.
1573
1574 Returns:
1575 A modified instance of http that was passed in.
1576
1577 Example:
1578
1579 h = httplib2.Http()
1580 h = set_user_agent(h, "my-app-name/6.0")
1581
1582 Most of the time the user-agent will be set doing auth, this is for the rare
1583 cases where you are accessing an unauthenticated endpoint.
1584 """
1585 request_orig = http.request
1586
1587 # The closure that will replace 'httplib2.Http.request'.
1588 def new_request(uri, method='GET', body=None, headers=None,
1589 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1590 connection_type=None):
1591 """Modify the request headers to add the user-agent."""
1592 if headers is None:
1593 headers = {}
1594 if 'user-agent' in headers:
1595 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1596 else:
1597 headers['user-agent'] = user_agent
1598 resp, content = request_orig(uri, method, body, headers,
1599 redirections, connection_type)
1600 return resp, content
1601
1602 http.request = new_request
1603 return http
1604
1605
1606def tunnel_patch(http):
1607 """Tunnel PATCH requests over POST.
1608 Args:
1609 http - An instance of httplib2.Http
1610 or something that acts like it.
1611
1612 Returns:
1613 A modified instance of http that was passed in.
1614
1615 Example:
1616
1617 h = httplib2.Http()
1618 h = tunnel_patch(h, "my-app-name/6.0")
1619
1620 Useful if you are running on a platform that doesn't support PATCH.
1621 Apply this last if you are using OAuth 1.0, as changing the method
1622 will result in a different signature.
1623 """
1624 request_orig = http.request
1625
1626 # The closure that will replace 'httplib2.Http.request'.
1627 def new_request(uri, method='GET', body=None, headers=None,
1628 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1629 connection_type=None):
1630 """Modify the request headers to add the user-agent."""
1631 if headers is None:
1632 headers = {}
1633 if method == 'PATCH':
1634 if 'oauth_token' in headers.get('authorization', ''):
1635 logging.warning(
1636 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1637 headers['x-http-method-override'] = "PATCH"
1638 method = 'POST'
1639 resp, content = request_orig(uri, method, body, headers,
1640 redirections, connection_type)
1641 return resp, content
1642
1643 http.request = new_request
1644 return http