blob: c3ef13b8083624bb4dda351455d14d3947a19d25 [file] [log] [blame]
Joe Gregorio88f699f2012-06-07 13:36:06 -04001# Copyright (C) 2012 Google Inc.
Joe Gregorio20a5aa92011-04-01 17:44:25 -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.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040014
Joe Gregorioaf276d22010-12-09 14:26:58 -050015"""Classes to encapsulate a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040016
Joe Gregorioaf276d22010-12-09 14:26:58 -050017The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actuall HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040020"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioaf276d22010-12-09 14:26:58 -050023
Joe Gregorio66f57522011-11-30 11:00:00 -050024import StringIO
Ali Afshar6f11ea12012-02-07 10:32:14 -050025import base64
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import copy
Joe Gregorio66f57522011-11-30 11:00:00 -050027import gzip
Joe Gregorioc6722462010-12-20 14:29:28 -050028import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import mimeparse
30import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050031import os
32import urllib
33import urlparse
34import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050035
Joe Gregorio654f4a22012-02-09 14:15:44 -050036from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050037from email.mime.multipart import MIMEMultipart
38from email.mime.nonmultipart import MIMENonMultipart
39from email.parser import FeedParser
40from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050041from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050042from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040043from errors import UnexpectedBodyError
44from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050045from model import JsonModel
Joe Gregorio549230c2012-01-11 10:38:05 -050046from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040047
48
Joe Gregorio910b9b12012-06-12 09:36:30 -040049DEFAULT_CHUNK_SIZE = 512*1024
50
51
Joe Gregoriod0bd3882011-11-22 09:49:47 -050052class MediaUploadProgress(object):
53 """Status of a resumable upload."""
54
55 def __init__(self, resumable_progress, total_size):
56 """Constructor.
57
58 Args:
59 resumable_progress: int, bytes sent so far.
Joe Gregorio910b9b12012-06-12 09:36:30 -040060 total_size: int, total bytes in complete upload, or None if the total
61 upload size isn't known ahead of time.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050062 """
63 self.resumable_progress = resumable_progress
64 self.total_size = total_size
65
66 def progress(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040067 """Percent of upload completed, as a float.
68
69 Returns:
70 the percentage complete as a float, returning 0.0 if the total size of
71 the upload is unknown.
72 """
73 if self.total_size is not None:
74 return float(self.resumable_progress) / float(self.total_size)
75 else:
76 return 0.0
Joe Gregoriod0bd3882011-11-22 09:49:47 -050077
78
Joe Gregorio708388c2012-06-15 13:43:04 -040079class MediaDownloadProgress(object):
80 """Status of a resumable download."""
81
82 def __init__(self, resumable_progress, total_size):
83 """Constructor.
84
85 Args:
86 resumable_progress: int, bytes received so far.
87 total_size: int, total bytes in complete download.
88 """
89 self.resumable_progress = resumable_progress
90 self.total_size = total_size
91
92 def progress(self):
93 """Percent of download completed, as a float.
94
95 Returns:
96 the percentage complete as a float, returning 0.0 if the total size of
97 the download is unknown.
98 """
99 if self.total_size is not None:
100 return float(self.resumable_progress) / float(self.total_size)
101 else:
102 return 0.0
103
104
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500105class MediaUpload(object):
106 """Describes a media object to upload.
107
108 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -0400109
110 Note that subclasses of MediaUpload may allow you to control the chunksize
111 when upload a media object. It is important to keep the size of the chunk as
112 large as possible to keep the upload efficient. Other factors may influence
113 the size of the chunk you use, particularly if you are working in an
114 environment where individual HTTP requests may have a hardcoded time limit,
115 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500116 """
117
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500118 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400119 """Chunk size for resumable uploads.
120
121 Returns:
122 Chunk size in bytes.
123 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500124 raise NotImplementedError()
125
126 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400127 """Mime type of the body.
128
129 Returns:
130 Mime type.
131 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500132 return 'application/octet-stream'
133
Joe Gregorio910b9b12012-06-12 09:36:30 -0400134 def size(self):
135 """Size of upload.
136
137 Returns:
138 Size of the body, or None of the size is unknown.
139 """
140 return None
141
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500142 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400143 """Whether this upload is resumable.
144
145 Returns:
146 True if resumable upload or False.
147 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500148 return False
149
Joe Gregorio910b9b12012-06-12 09:36:30 -0400150 def getbytes(self, begin, end):
151 """Get bytes from the media.
152
153 Args:
154 begin: int, offset from beginning of file.
155 length: int, number of bytes to read, starting at begin.
156
157 Returns:
158 A string of bytes read. May be shorter than length if EOF was reached
159 first.
160 """
161 raise NotImplementedError()
162
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500163 def _to_json(self, strip=None):
164 """Utility function for creating a JSON representation of a MediaUpload.
165
166 Args:
167 strip: array, An array of names of members to not include in the JSON.
168
169 Returns:
170 string, a JSON representation of this instance, suitable to pass to
171 from_json().
172 """
173 t = type(self)
174 d = copy.copy(self.__dict__)
175 if strip is not None:
176 for member in strip:
177 del d[member]
178 d['_class'] = t.__name__
179 d['_module'] = t.__module__
180 return simplejson.dumps(d)
181
182 def to_json(self):
183 """Create a JSON representation of an instance of MediaUpload.
184
185 Returns:
186 string, a JSON representation of this instance, suitable to pass to
187 from_json().
188 """
189 return self._to_json()
190
191 @classmethod
192 def new_from_json(cls, s):
193 """Utility class method to instantiate a MediaUpload subclass from a JSON
194 representation produced by to_json().
195
196 Args:
197 s: string, JSON from to_json().
198
199 Returns:
200 An instance of the subclass of MediaUpload that was serialized with
201 to_json().
202 """
203 data = simplejson.loads(s)
204 # Find and call the right classmethod from_json() to restore the object.
205 module = data['_module']
206 m = __import__(module, fromlist=module.split('.')[:-1])
207 kls = getattr(m, data['_class'])
208 from_json = getattr(kls, 'from_json')
209 return from_json(s)
210
Joe Gregorio66f57522011-11-30 11:00:00 -0500211
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500212class MediaFileUpload(MediaUpload):
213 """A MediaUpload for a file.
214
215 Construct a MediaFileUpload and pass as the media_body parameter of the
216 method. For example, if we had a service that allowed uploading images:
217
218
Joe Gregorio910b9b12012-06-12 09:36:30 -0400219 media = MediaFileUpload('smiley.png', mimetype='image/png',
220 chunksize=1024*1024, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500221 service.objects().insert(
222 bucket=buckets['items'][0]['id'],
223 name='smiley.png',
224 media_body=media).execute()
225 """
226
Joe Gregorio910b9b12012-06-12 09:36:30 -0400227 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500228 """Constructor.
229
230 Args:
231 filename: string, Name of the file.
232 mimetype: string, Mime-type of the file. If None then a mime-type will be
233 guessed from the file extension.
234 chunksize: int, File will be uploaded in chunks of this many bytes. Only
235 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500236 resumable: bool, True if this is a resumable upload. False means upload
237 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500238 """
239 self._filename = filename
240 self._size = os.path.getsize(filename)
241 self._fd = None
242 if mimetype is None:
243 (mimetype, encoding) = mimetypes.guess_type(filename)
244 self._mimetype = mimetype
245 self._chunksize = chunksize
246 self._resumable = resumable
247
Joe Gregorio910b9b12012-06-12 09:36:30 -0400248 def chunksize(self):
249 """Chunk size for resumable uploads.
250
251 Returns:
252 Chunk size in bytes.
253 """
254 return self._chunksize
255
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500256 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400257 """Mime type of the body.
258
259 Returns:
260 Mime type.
261 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500262 return self._mimetype
263
264 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400265 """Size of upload.
266
267 Returns:
268 Size of the body, or None of the size is unknown.
269 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500270 return self._size
271
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500272 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400273 """Whether this upload is resumable.
274
275 Returns:
276 True if resumable upload or False.
277 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500278 return self._resumable
279
280 def getbytes(self, begin, length):
281 """Get bytes from the media.
282
283 Args:
284 begin: int, offset from beginning of file.
285 length: int, number of bytes to read, starting at begin.
286
287 Returns:
288 A string of bytes read. May be shorted than length if EOF was reached
289 first.
290 """
291 if self._fd is None:
292 self._fd = open(self._filename, 'rb')
293 self._fd.seek(begin)
294 return self._fd.read(length)
295
296 def to_json(self):
Joe Gregorio708388c2012-06-15 13:43:04 -0400297 """Creating a JSON representation of an instance of MediaFileUpload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500298
299 Returns:
300 string, a JSON representation of this instance, suitable to pass to
301 from_json().
302 """
303 return self._to_json(['_fd'])
304
305 @staticmethod
306 def from_json(s):
307 d = simplejson.loads(s)
308 return MediaFileUpload(
309 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
310
311
Joe Gregorio910b9b12012-06-12 09:36:30 -0400312class MediaIoBaseUpload(MediaUpload):
313 """A MediaUpload for a io.Base objects.
314
315 Note that the Python file object is compatible with io.Base and can be used
316 with this class also.
317
Joe Gregorio910b9b12012-06-12 09:36:30 -0400318 fh = io.BytesIO('...Some data to upload...')
319 media = MediaIoBaseUpload(fh, mimetype='image/png',
320 chunksize=1024*1024, resumable=True)
321 service.objects().insert(
322 bucket='a_bucket_id',
323 name='smiley.png',
324 media_body=media).execute()
325 """
326
327 def __init__(self, fh, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
328 resumable=False):
329 """Constructor.
330
331 Args:
Joe Gregorio44454e42012-06-15 08:38:53 -0400332 fh: io.Base or file object, The source of the bytes to upload. MUST be
333 opened in blocking mode, do not use streams opened in non-blocking mode.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400334 mimetype: string, Mime-type of the file. If None then a mime-type will be
335 guessed from the file extension.
336 chunksize: int, File will be uploaded in chunks of this many bytes. Only
337 used if resumable=True.
338 resumable: bool, True if this is a resumable upload. False means upload
339 in a single request.
340 """
341 self._fh = fh
342 self._mimetype = mimetype
343 self._chunksize = chunksize
344 self._resumable = resumable
345 self._size = None
346 try:
347 if hasattr(self._fh, 'fileno'):
348 fileno = self._fh.fileno()
Joe Gregorio44454e42012-06-15 08:38:53 -0400349
350 # Pipes and such show up as 0 length files.
351 size = os.fstat(fileno).st_size
352 if size:
353 self._size = os.fstat(fileno).st_size
Joe Gregorio910b9b12012-06-12 09:36:30 -0400354 except IOError:
355 pass
356
357 def chunksize(self):
358 """Chunk size for resumable uploads.
359
360 Returns:
361 Chunk size in bytes.
362 """
363 return self._chunksize
364
365 def mimetype(self):
366 """Mime type of the body.
367
368 Returns:
369 Mime type.
370 """
371 return self._mimetype
372
373 def size(self):
374 """Size of upload.
375
376 Returns:
377 Size of the body, or None of the size is unknown.
378 """
379 return self._size
380
381 def resumable(self):
382 """Whether this upload is resumable.
383
384 Returns:
385 True if resumable upload or False.
386 """
387 return self._resumable
388
389 def getbytes(self, begin, length):
390 """Get bytes from the media.
391
392 Args:
393 begin: int, offset from beginning of file.
394 length: int, number of bytes to read, starting at begin.
395
396 Returns:
397 A string of bytes read. May be shorted than length if EOF was reached
398 first.
399 """
400 self._fh.seek(begin)
401 return self._fh.read(length)
402
403 def to_json(self):
404 """This upload type is not serializable."""
405 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
406
407
Ali Afshar6f11ea12012-02-07 10:32:14 -0500408class MediaInMemoryUpload(MediaUpload):
409 """MediaUpload for a chunk of bytes.
410
411 Construct a MediaFileUpload and pass as the media_body parameter of the
412 method. For example, if we had a service that allowed plain text:
413 """
414
415 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400416 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500417 """Create a new MediaBytesUpload.
418
419 Args:
420 body: string, Bytes of body content.
421 mimetype: string, Mime-type of the file or default of
422 'application/octet-stream'.
423 chunksize: int, File will be uploaded in chunks of this many bytes. Only
424 used if resumable=True.
425 resumable: bool, True if this is a resumable upload. False means upload
426 in a single request.
427 """
428 self._body = body
429 self._mimetype = mimetype
430 self._resumable = resumable
431 self._chunksize = chunksize
432
433 def chunksize(self):
434 """Chunk size for resumable uploads.
435
436 Returns:
437 Chunk size in bytes.
438 """
439 return self._chunksize
440
441 def mimetype(self):
442 """Mime type of the body.
443
444 Returns:
445 Mime type.
446 """
447 return self._mimetype
448
449 def size(self):
450 """Size of upload.
451
452 Returns:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400453 Size of the body, or None of the size is unknown.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500454 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400455 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500456
457 def resumable(self):
458 """Whether this upload is resumable.
459
460 Returns:
461 True if resumable upload or False.
462 """
463 return self._resumable
464
465 def getbytes(self, begin, length):
466 """Get bytes from the media.
467
468 Args:
469 begin: int, offset from beginning of file.
470 length: int, number of bytes to read, starting at begin.
471
472 Returns:
473 A string of bytes read. May be shorter than length if EOF was reached
474 first.
475 """
476 return self._body[begin:begin + length]
477
478 def to_json(self):
479 """Create a JSON representation of a MediaInMemoryUpload.
480
481 Returns:
482 string, a JSON representation of this instance, suitable to pass to
483 from_json().
484 """
485 t = type(self)
486 d = copy.copy(self.__dict__)
487 del d['_body']
488 d['_class'] = t.__name__
489 d['_module'] = t.__module__
490 d['_b64body'] = base64.b64encode(self._body)
491 return simplejson.dumps(d)
492
493 @staticmethod
494 def from_json(s):
495 d = simplejson.loads(s)
496 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
497 d['_mimetype'], d['_chunksize'],
498 d['_resumable'])
499
500
Joe Gregorio708388c2012-06-15 13:43:04 -0400501class MediaIoBaseDownload(object):
502 """"Download media resources.
503
504 Note that the Python file object is compatible with io.Base and can be used
505 with this class also.
506
507
508 Example:
509 request = service.objects().get_media(
510 bucket='a_bucket_id',
511 name='smiley.png')
512
513 fh = io.FileIO('image.png', mode='wb')
514 downloader = MediaIoBaseDownload(fh, request, chunksize=1024*1024)
515
516 done = False
517 while done is False:
518 status, done = downloader.next_chunk()
519 if status:
520 print "Download %d%%." % int(status.progress() * 100)
521 print "Download Complete!"
522 """
523
524 def __init__(self, fh, request, chunksize=DEFAULT_CHUNK_SIZE):
525 """Constructor.
526
527 Args:
528 fh: io.Base or file object, The stream in which to write the downloaded
529 bytes.
530 request: apiclient.http.HttpRequest, the media request to perform in
531 chunks.
532 chunksize: int, File will be downloaded in chunks of this many bytes.
533 """
534 self.fh_ = fh
535 self.request_ = request
536 self.uri_ = request.uri
537 self.chunksize_ = chunksize
538 self.progress_ = 0
539 self.total_size_ = None
540 self.done_ = False
541
542 def next_chunk(self):
543 """Get the next chunk of the download.
544
545 Returns:
546 (status, done): (MediaDownloadStatus, boolean)
547 The value of 'done' will be True when the media has been fully
548 downloaded.
549
550 Raises:
551 apiclient.errors.HttpError if the response was not a 2xx.
552 httplib2.Error if a transport error has occured.
553 """
554 headers = {
555 'range': 'bytes=%d-%d' % (
556 self.progress_, self.progress_ + self.chunksize_)
557 }
558 http = self.request_.http
559 http.follow_redirects = False
560
561 resp, content = http.request(self.uri_, headers=headers)
562 if resp.status in [301, 302, 303, 307, 308] and 'location' in resp:
563 self.uri_ = resp['location']
564 resp, content = http.request(self.uri_, headers=headers)
565 if resp.status in [200, 206]:
566 self.progress_ += len(content)
567 self.fh_.write(content)
568
569 if 'content-range' in resp:
570 content_range = resp['content-range']
571 length = content_range.rsplit('/', 1)[1]
572 self.total_size_ = int(length)
573
574 if self.progress_ == self.total_size_:
575 self.done_ = True
576 return MediaDownloadProgress(self.progress_, self.total_size_), self.done_
577 else:
578 raise HttpError(resp, content, self.uri_)
579
580
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400581class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500582 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400583
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500584 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500585 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500586 body=None,
587 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500588 methodId=None,
589 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500590 """Constructor for an HttpRequest.
591
Joe Gregorioaf276d22010-12-09 14:26:58 -0500592 Args:
593 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500594 postproc: callable, called on the HTTP response and content to transform
595 it into a data object before returning, or raising an exception
596 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500597 uri: string, the absolute URI to send the request to
598 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500599 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500600 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500601 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500602 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500603 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400604 self.uri = uri
605 self.method = method
606 self.body = body
607 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500608 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400609 self.http = http
610 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500611 self.resumable = resumable
Joe Gregorio910b9b12012-06-12 09:36:30 -0400612 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500613
Joe Gregorio66f57522011-11-30 11:00:00 -0500614 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500615 major, minor, params = mimeparse.parse_mime_type(
616 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500617
Joe Gregorio945be3e2012-01-27 17:01:06 -0500618 # The size of the non-media part of the request.
619 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500620
621 # The resumable URI to send chunks to.
622 self.resumable_uri = None
623
624 # The bytes that have been uploaded.
625 self.resumable_progress = 0
626
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400627 def execute(self, http=None):
628 """Execute the request.
629
Joe Gregorioaf276d22010-12-09 14:26:58 -0500630 Args:
631 http: httplib2.Http, an http object to be used in place of the
632 one the HttpRequest request object was constructed with.
633
634 Returns:
635 A deserialized object model of the response body as determined
636 by the postproc.
637
638 Raises:
639 apiclient.errors.HttpError if the response was not a 2xx.
640 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400641 """
642 if http is None:
643 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500644 if self.resumable:
645 body = None
646 while body is None:
647 _, body = self.next_chunk(http)
648 return body
649 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500650 if 'content-length' not in self.headers:
651 self.headers['content-length'] = str(self.body_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500652 resp, content = http.request(self.uri, self.method,
653 body=self.body,
654 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500655
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500656 if resp.status >= 300:
657 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400658 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500659
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500660 def next_chunk(self, http=None):
661 """Execute the next step of a resumable upload.
662
Joe Gregorio66f57522011-11-30 11:00:00 -0500663 Can only be used if the method being executed supports media uploads and
664 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500665
666 Example:
667
Joe Gregorio66f57522011-11-30 11:00:00 -0500668 media = MediaFileUpload('smiley.png', mimetype='image/png',
669 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500670 request = service.objects().insert(
671 bucket=buckets['items'][0]['id'],
672 name='smiley.png',
673 media_body=media)
674
675 response = None
676 while response is None:
677 status, response = request.next_chunk()
678 if status:
679 print "Upload %d%% complete." % int(status.progress() * 100)
680
681
682 Returns:
683 (status, body): (ResumableMediaStatus, object)
684 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400685
686 Raises:
687 apiclient.errors.HttpError if the response was not a 2xx.
688 httplib2.Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500689 """
690 if http is None:
691 http = self.http
692
Joe Gregorio910b9b12012-06-12 09:36:30 -0400693 if self.resumable.size() is None:
694 size = '*'
695 else:
696 size = str(self.resumable.size())
697
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500698 if self.resumable_uri is None:
699 start_headers = copy.copy(self.headers)
700 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400701 if size != '*':
702 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500703 start_headers['content-length'] = str(self.body_size)
704
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500705 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500706 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500707 headers=start_headers)
708 if resp.status == 200 and 'location' in resp:
709 self.resumable_uri = resp['location']
710 else:
711 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregorio910b9b12012-06-12 09:36:30 -0400712 elif self._in_error_state:
713 # If we are in an error state then query the server for current state of
714 # the upload by sending an empty PUT and reading the 'range' header in
715 # the response.
716 headers = {
717 'Content-Range': 'bytes */%s' % size,
718 'content-length': '0'
719 }
720 resp, content = http.request(self.resumable_uri, 'PUT',
721 headers=headers)
722 status, body = self._process_response(resp, content)
723 if body:
724 # The upload was complete.
725 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500726
Joe Gregorio910b9b12012-06-12 09:36:30 -0400727 data = self.resumable.getbytes(
728 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400729
730 # A short read implies that we are at EOF, so finish the upload.
731 if len(data) < self.resumable.chunksize():
732 size = str(self.resumable_progress + len(data))
733
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500734 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400735 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500736 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400737 size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500738 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400739 try:
740 resp, content = http.request(self.resumable_uri, 'PUT',
741 body=data,
742 headers=headers)
743 except:
744 self._in_error_state = True
745 raise
746
747 return self._process_response(resp, content)
748
749 def _process_response(self, resp, content):
750 """Process the response from a single chunk upload.
751
752 Args:
753 resp: httplib2.Response, the response object.
754 content: string, the content of the response.
755
756 Returns:
757 (status, body): (ResumableMediaStatus, object)
758 The body will be None until the resumable media is fully uploaded.
759
760 Raises:
761 apiclient.errors.HttpError if the response was not a 2xx or a 308.
762 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500763 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400764 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500765 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500766 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400767 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500768 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500769 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500770 if 'location' in resp:
771 self.resumable_uri = resp['location']
772 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400773 self._in_error_state = True
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500774 raise HttpError(resp, content, self.uri)
775
Joe Gregorio945be3e2012-01-27 17:01:06 -0500776 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
777 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500778
779 def to_json(self):
780 """Returns a JSON representation of the HttpRequest."""
781 d = copy.copy(self.__dict__)
782 if d['resumable'] is not None:
783 d['resumable'] = self.resumable.to_json()
784 del d['http']
785 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400786
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500787 return simplejson.dumps(d)
788
789 @staticmethod
790 def from_json(s, http, postproc):
791 """Returns an HttpRequest populated with info from a JSON object."""
792 d = simplejson.loads(s)
793 if d['resumable'] is not None:
794 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
795 return HttpRequest(
796 http,
797 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500798 uri=d['uri'],
799 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500800 body=d['body'],
801 headers=d['headers'],
802 methodId=d['methodId'],
803 resumable=d['resumable'])
804
Joe Gregorioaf276d22010-12-09 14:26:58 -0500805
Joe Gregorio66f57522011-11-30 11:00:00 -0500806class BatchHttpRequest(object):
807 """Batches multiple HttpRequest objects into a single HTTP request."""
808
809 def __init__(self, callback=None, batch_uri=None):
810 """Constructor for a BatchHttpRequest.
811
812 Args:
813 callback: callable, A callback to be called for each response, of the
814 form callback(id, response). The first parameter is the request id, and
815 the second is the deserialized response object.
816 batch_uri: string, URI to send batch requests to.
817 """
818 if batch_uri is None:
819 batch_uri = 'https://www.googleapis.com/batch'
820 self._batch_uri = batch_uri
821
822 # Global callback to be called for each individual response in the batch.
823 self._callback = callback
824
Joe Gregorio654f4a22012-02-09 14:15:44 -0500825 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500826 self._requests = {}
827
Joe Gregorio654f4a22012-02-09 14:15:44 -0500828 # A map from id to callback.
829 self._callbacks = {}
830
Joe Gregorio66f57522011-11-30 11:00:00 -0500831 # List of request ids, in the order in which they were added.
832 self._order = []
833
834 # The last auto generated id.
835 self._last_auto_id = 0
836
837 # Unique ID on which to base the Content-ID headers.
838 self._base_id = None
839
Joe Gregorio654f4a22012-02-09 14:15:44 -0500840 # A map from request id to (headers, content) response pairs
841 self._responses = {}
842
843 # A map of id(Credentials) that have been refreshed.
844 self._refreshed_credentials = {}
845
846 def _refresh_and_apply_credentials(self, request, http):
847 """Refresh the credentials and apply to the request.
848
849 Args:
850 request: HttpRequest, the request.
851 http: httplib2.Http, the global http object for the batch.
852 """
853 # For the credentials to refresh, but only once per refresh_token
854 # If there is no http per the request then refresh the http passed in
855 # via execute()
856 creds = None
857 if request.http is not None and hasattr(request.http.request,
858 'credentials'):
859 creds = request.http.request.credentials
860 elif http is not None and hasattr(http.request, 'credentials'):
861 creds = http.request.credentials
862 if creds is not None:
863 if id(creds) not in self._refreshed_credentials:
864 creds.refresh(http)
865 self._refreshed_credentials[id(creds)] = 1
866
867 # Only apply the credentials if we are using the http object passed in,
868 # otherwise apply() will get called during _serialize_request().
869 if request.http is None or not hasattr(request.http.request,
870 'credentials'):
871 creds.apply(request.headers)
872
Joe Gregorio66f57522011-11-30 11:00:00 -0500873 def _id_to_header(self, id_):
874 """Convert an id to a Content-ID header value.
875
876 Args:
877 id_: string, identifier of individual request.
878
879 Returns:
880 A Content-ID header with the id_ encoded into it. A UUID is prepended to
881 the value because Content-ID headers are supposed to be universally
882 unique.
883 """
884 if self._base_id is None:
885 self._base_id = uuid.uuid4()
886
887 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
888
889 def _header_to_id(self, header):
890 """Convert a Content-ID header value to an id.
891
892 Presumes the Content-ID header conforms to the format that _id_to_header()
893 returns.
894
895 Args:
896 header: string, Content-ID header value.
897
898 Returns:
899 The extracted id value.
900
901 Raises:
902 BatchError if the header is not in the expected format.
903 """
904 if header[0] != '<' or header[-1] != '>':
905 raise BatchError("Invalid value for Content-ID: %s" % header)
906 if '+' not in header:
907 raise BatchError("Invalid value for Content-ID: %s" % header)
908 base, id_ = header[1:-1].rsplit('+', 1)
909
910 return urllib.unquote(id_)
911
912 def _serialize_request(self, request):
913 """Convert an HttpRequest object into a string.
914
915 Args:
916 request: HttpRequest, the request to serialize.
917
918 Returns:
919 The request as a string in application/http format.
920 """
921 # Construct status line
922 parsed = urlparse.urlparse(request.uri)
923 request_line = urlparse.urlunparse(
924 (None, None, parsed.path, parsed.params, parsed.query, None)
925 )
926 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500927 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500928 msg = MIMENonMultipart(major, minor)
929 headers = request.headers.copy()
930
Joe Gregorio654f4a22012-02-09 14:15:44 -0500931 if request.http is not None and hasattr(request.http.request,
932 'credentials'):
933 request.http.request.credentials.apply(headers)
934
Joe Gregorio66f57522011-11-30 11:00:00 -0500935 # MIMENonMultipart adds its own Content-Type header.
936 if 'content-type' in headers:
937 del headers['content-type']
938
939 for key, value in headers.iteritems():
940 msg[key] = value
941 msg['Host'] = parsed.netloc
942 msg.set_unixfrom(None)
943
944 if request.body is not None:
945 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500946 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500947
Joe Gregorio654f4a22012-02-09 14:15:44 -0500948 # Serialize the mime message.
949 fp = StringIO.StringIO()
950 # maxheaderlen=0 means don't line wrap headers.
951 g = Generator(fp, maxheaderlen=0)
952 g.flatten(msg, unixfrom=False)
953 body = fp.getvalue()
954
Joe Gregorio66f57522011-11-30 11:00:00 -0500955 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
956 if request.body is None:
957 body = body[:-2]
958
Joe Gregoriodd813822012-01-25 10:32:47 -0500959 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500960
961 def _deserialize_response(self, payload):
962 """Convert string into httplib2 response and content.
963
964 Args:
965 payload: string, headers and body as a string.
966
967 Returns:
968 A pair (resp, content) like would be returned from httplib2.request.
969 """
970 # Strip off the status line
971 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500972 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500973
974 # Parse the rest of the response
975 parser = FeedParser()
976 parser.feed(payload)
977 msg = parser.close()
978 msg['status'] = status
979
980 # Create httplib2.Response from the parsed headers.
981 resp = httplib2.Response(msg)
982 resp.reason = reason
983 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
984
985 content = payload.split('\r\n\r\n', 1)[1]
986
987 return resp, content
988
989 def _new_id(self):
990 """Create a new id.
991
992 Auto incrementing number that avoids conflicts with ids already used.
993
994 Returns:
995 string, a new unique id.
996 """
997 self._last_auto_id += 1
998 while str(self._last_auto_id) in self._requests:
999 self._last_auto_id += 1
1000 return str(self._last_auto_id)
1001
1002 def add(self, request, callback=None, request_id=None):
1003 """Add a new request.
1004
1005 Every callback added will be paired with a unique id, the request_id. That
1006 unique id will be passed back to the callback when the response comes back
1007 from the server. The default behavior is to have the library generate it's
1008 own unique id. If the caller passes in a request_id then they must ensure
1009 uniqueness for each request_id, and if they are not an exception is
1010 raised. Callers should either supply all request_ids or nevery supply a
1011 request id, to avoid such an error.
1012
1013 Args:
1014 request: HttpRequest, Request to add to the batch.
1015 callback: callable, A callback to be called for this response, of the
1016 form callback(id, response). The first parameter is the request id, and
1017 the second is the deserialized response object.
1018 request_id: string, A unique id for the request. The id will be passed to
1019 the callback with the response.
1020
1021 Returns:
1022 None
1023
1024 Raises:
1025 BatchError if a resumable request is added to a batch.
1026 KeyError is the request_id is not unique.
1027 """
1028 if request_id is None:
1029 request_id = self._new_id()
1030 if request.resumable is not None:
1031 raise BatchError("Resumable requests cannot be used in a batch request.")
1032 if request_id in self._requests:
1033 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -05001034 self._requests[request_id] = request
1035 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -05001036 self._order.append(request_id)
1037
Joe Gregorio654f4a22012-02-09 14:15:44 -05001038 def _execute(self, http, order, requests):
1039 """Serialize batch request, send to server, process response.
1040
1041 Args:
1042 http: httplib2.Http, an http object to be used to make the request with.
1043 order: list, list of request ids in the order they were added to the
1044 batch.
1045 request: list, list of request objects to send.
1046
1047 Raises:
1048 httplib2.Error if a transport error has occured.
1049 apiclient.errors.BatchError if the response is the wrong format.
1050 """
1051 message = MIMEMultipart('mixed')
1052 # Message should not write out it's own headers.
1053 setattr(message, '_write_headers', lambda self: None)
1054
1055 # Add all the individual requests.
1056 for request_id in order:
1057 request = requests[request_id]
1058
1059 msg = MIMENonMultipart('application', 'http')
1060 msg['Content-Transfer-Encoding'] = 'binary'
1061 msg['Content-ID'] = self._id_to_header(request_id)
1062
1063 body = self._serialize_request(request)
1064 msg.set_payload(body)
1065 message.attach(msg)
1066
1067 body = message.as_string()
1068
1069 headers = {}
1070 headers['content-type'] = ('multipart/mixed; '
1071 'boundary="%s"') % message.get_boundary()
1072
1073 resp, content = http.request(self._batch_uri, 'POST', body=body,
1074 headers=headers)
1075
1076 if resp.status >= 300:
1077 raise HttpError(resp, content, self._batch_uri)
1078
1079 # Now break out the individual responses and store each one.
1080 boundary, _ = content.split(None, 1)
1081
1082 # Prepend with a content-type header so FeedParser can handle it.
1083 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1084 for_parser = header + content
1085
1086 parser = FeedParser()
1087 parser.feed(for_parser)
1088 mime_response = parser.close()
1089
1090 if not mime_response.is_multipart():
1091 raise BatchError("Response not in multipart/mixed format.", resp,
1092 content)
1093
1094 for part in mime_response.get_payload():
1095 request_id = self._header_to_id(part['Content-ID'])
1096 headers, content = self._deserialize_response(part.get_payload())
1097 self._responses[request_id] = (headers, content)
1098
Joe Gregorio66f57522011-11-30 11:00:00 -05001099 def execute(self, http=None):
1100 """Execute all the requests as a single batched HTTP request.
1101
1102 Args:
1103 http: httplib2.Http, an http object to be used in place of the one the
1104 HttpRequest request object was constructed with. If one isn't supplied
1105 then use a http object from the requests in this batch.
1106
1107 Returns:
1108 None
1109
1110 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -05001111 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001112 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001113 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001114
1115 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001116 if http is None:
1117 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001118 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001119 if request is not None:
1120 http = request.http
1121 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001122
Joe Gregorio66f57522011-11-30 11:00:00 -05001123 if http is None:
1124 raise ValueError("Missing a valid http object.")
1125
Joe Gregorio654f4a22012-02-09 14:15:44 -05001126 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001127
Joe Gregorio654f4a22012-02-09 14:15:44 -05001128 # Loop over all the requests and check for 401s. For each 401 request the
1129 # credentials should be refreshed and then sent again in a separate batch.
1130 redo_requests = {}
1131 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001132
Joe Gregorio66f57522011-11-30 11:00:00 -05001133 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001134 headers, content = self._responses[request_id]
1135 if headers['status'] == '401':
1136 redo_order.append(request_id)
1137 request = self._requests[request_id]
1138 self._refresh_and_apply_credentials(request, http)
1139 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001140
Joe Gregorio654f4a22012-02-09 14:15:44 -05001141 if redo_requests:
1142 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001143
Joe Gregorio654f4a22012-02-09 14:15:44 -05001144 # Now process all callbacks that are erroring, and raise an exception for
1145 # ones that return a non-2xx response? Or add extra parameter to callback
1146 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001147
Joe Gregorio654f4a22012-02-09 14:15:44 -05001148 for request_id in self._order:
1149 headers, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001150
Joe Gregorio654f4a22012-02-09 14:15:44 -05001151 request = self._requests[request_id]
1152 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001153
Joe Gregorio654f4a22012-02-09 14:15:44 -05001154 response = None
1155 exception = None
1156 try:
1157 r = httplib2.Response(headers)
1158 response = request.postproc(r, content)
1159 except HttpError, e:
1160 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001161
Joe Gregorio654f4a22012-02-09 14:15:44 -05001162 if callback is not None:
1163 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001164 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001165 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001166
1167
Joe Gregorioaf276d22010-12-09 14:26:58 -05001168class HttpRequestMock(object):
1169 """Mock of HttpRequest.
1170
1171 Do not construct directly, instead use RequestMockBuilder.
1172 """
1173
1174 def __init__(self, resp, content, postproc):
1175 """Constructor for HttpRequestMock
1176
1177 Args:
1178 resp: httplib2.Response, the response to emulate coming from the request
1179 content: string, the response body
1180 postproc: callable, the post processing function usually supplied by
1181 the model class. See model.JsonModel.response() as an example.
1182 """
1183 self.resp = resp
1184 self.content = content
1185 self.postproc = postproc
1186 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001187 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001188 if 'reason' in self.resp:
1189 self.resp.reason = self.resp['reason']
1190
1191 def execute(self, http=None):
1192 """Execute the request.
1193
1194 Same behavior as HttpRequest.execute(), but the response is
1195 mocked and not really from an HTTP request/response.
1196 """
1197 return self.postproc(self.resp, self.content)
1198
1199
1200class RequestMockBuilder(object):
1201 """A simple mock of HttpRequest
1202
1203 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001204 tuples of (httplib2.Response, content, opt_expected_body) that should be
1205 returned when that method is called. None may also be passed in for the
1206 httplib2.Response, in which case a 200 OK response will be generated.
1207 If an opt_expected_body (str or dict) is provided, it will be compared to
1208 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001209
1210 Example:
1211 response = '{"data": {"id": "tag:google.c...'
1212 requestBuilder = RequestMockBuilder(
1213 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001214 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001215 }
1216 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001217 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001218
1219 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001220 200 OK with an empty string as the response content or raise an excpetion
1221 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001222 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001223
1224 For more details see the project wiki.
1225 """
1226
Joe Gregorioa388ce32011-09-09 17:19:13 -04001227 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001228 """Constructor for RequestMockBuilder
1229
1230 The constructed object should be a callable object
1231 that can replace the class HttpResponse.
1232
1233 responses - A dictionary that maps methodIds into tuples
1234 of (httplib2.Response, content). The methodId
1235 comes from the 'rpcName' field in the discovery
1236 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001237 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1238 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001239 """
1240 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001241 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001242
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001243 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001244 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001245 """Implements the callable interface that discovery.build() expects
1246 of requestBuilder, which is to build an object compatible with
1247 HttpRequest.execute(). See that method for the description of the
1248 parameters and the expected response.
1249 """
1250 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001251 response = self.responses[methodId]
1252 resp, content = response[:2]
1253 if len(response) > 2:
1254 # Test the body against the supplied expected_body.
1255 expected_body = response[2]
1256 if bool(expected_body) != bool(body):
1257 # Not expecting a body and provided one
1258 # or expecting a body and not provided one.
1259 raise UnexpectedBodyError(expected_body, body)
1260 if isinstance(expected_body, str):
1261 expected_body = simplejson.loads(expected_body)
1262 body = simplejson.loads(body)
1263 if body != expected_body:
1264 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001265 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001266 elif self.check_unexpected:
1267 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001268 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001269 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001270 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001271
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001272
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001273class HttpMock(object):
1274 """Mock of httplib2.Http"""
1275
Joe Gregorioec343652011-02-16 16:52:51 -05001276 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001277 """
1278 Args:
1279 filename: string, absolute filename to read response from
1280 headers: dict, header to return with response
1281 """
Joe Gregorioec343652011-02-16 16:52:51 -05001282 if headers is None:
1283 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001284 f = file(filename, 'r')
1285 self.data = f.read()
1286 f.close()
1287 self.headers = headers
1288
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001289 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001290 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001291 body=None,
1292 headers=None,
1293 redirections=1,
1294 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001295 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001296
1297
1298class HttpMockSequence(object):
1299 """Mock of httplib2.Http
1300
1301 Mocks a sequence of calls to request returning different responses for each
1302 call. Create an instance initialized with the desired response headers
1303 and content and then use as if an httplib2.Http instance.
1304
1305 http = HttpMockSequence([
1306 ({'status': '401'}, ''),
1307 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1308 ({'status': '200'}, 'echo_request_headers'),
1309 ])
1310 resp, content = http.request("http://examples.com")
1311
1312 There are special values you can pass in for content to trigger
1313 behavours that are helpful in testing.
1314
1315 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001316 'echo_request_headers_as_json' means return the request headers in
1317 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001318 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001319 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001320 """
1321
1322 def __init__(self, iterable):
1323 """
1324 Args:
1325 iterable: iterable, a sequence of pairs of (headers, body)
1326 """
1327 self._iterable = iterable
Joe Gregorio708388c2012-06-15 13:43:04 -04001328 self.follow_redirects = True
Joe Gregorioccc79542011-02-19 00:05:26 -05001329
1330 def request(self, uri,
1331 method='GET',
1332 body=None,
1333 headers=None,
1334 redirections=1,
1335 connection_type=None):
1336 resp, content = self._iterable.pop(0)
1337 if content == 'echo_request_headers':
1338 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001339 elif content == 'echo_request_headers_as_json':
1340 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001341 elif content == 'echo_request_body':
1342 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001343 elif content == 'echo_request_uri':
1344 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001345 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001346
1347
1348def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001349 """Set the user-agent on every request.
1350
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001351 Args:
1352 http - An instance of httplib2.Http
1353 or something that acts like it.
1354 user_agent: string, the value for the user-agent header.
1355
1356 Returns:
1357 A modified instance of http that was passed in.
1358
1359 Example:
1360
1361 h = httplib2.Http()
1362 h = set_user_agent(h, "my-app-name/6.0")
1363
1364 Most of the time the user-agent will be set doing auth, this is for the rare
1365 cases where you are accessing an unauthenticated endpoint.
1366 """
1367 request_orig = http.request
1368
1369 # The closure that will replace 'httplib2.Http.request'.
1370 def new_request(uri, method='GET', body=None, headers=None,
1371 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1372 connection_type=None):
1373 """Modify the request headers to add the user-agent."""
1374 if headers is None:
1375 headers = {}
1376 if 'user-agent' in headers:
1377 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1378 else:
1379 headers['user-agent'] = user_agent
1380 resp, content = request_orig(uri, method, body, headers,
1381 redirections, connection_type)
1382 return resp, content
1383
1384 http.request = new_request
1385 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001386
1387
1388def tunnel_patch(http):
1389 """Tunnel PATCH requests over POST.
1390 Args:
1391 http - An instance of httplib2.Http
1392 or something that acts like it.
1393
1394 Returns:
1395 A modified instance of http that was passed in.
1396
1397 Example:
1398
1399 h = httplib2.Http()
1400 h = tunnel_patch(h, "my-app-name/6.0")
1401
1402 Useful if you are running on a platform that doesn't support PATCH.
1403 Apply this last if you are using OAuth 1.0, as changing the method
1404 will result in a different signature.
1405 """
1406 request_orig = http.request
1407
1408 # The closure that will replace 'httplib2.Http.request'.
1409 def new_request(uri, method='GET', body=None, headers=None,
1410 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1411 connection_type=None):
1412 """Modify the request headers to add the user-agent."""
1413 if headers is None:
1414 headers = {}
1415 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001416 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001417 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001418 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001419 headers['x-http-method-override'] = "PATCH"
1420 method = 'POST'
1421 resp, content = request_orig(uri, method, body, headers,
1422 redirections, connection_type)
1423 return resp, content
1424
1425 http.request = new_request
1426 return http