blob: 022bdcee5431a4b8c688f5b62d29d19bb85c7385 [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
79class MediaUpload(object):
80 """Describes a media object to upload.
81
82 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -040083
84 Note that subclasses of MediaUpload may allow you to control the chunksize
85 when upload a media object. It is important to keep the size of the chunk as
86 large as possible to keep the upload efficient. Other factors may influence
87 the size of the chunk you use, particularly if you are working in an
88 environment where individual HTTP requests may have a hardcoded time limit,
89 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050090 """
91
Joe Gregoriod0bd3882011-11-22 09:49:47 -050092 def chunksize(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -040093 """Chunk size for resumable uploads.
94
95 Returns:
96 Chunk size in bytes.
97 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -050098 raise NotImplementedError()
99
100 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400101 """Mime type of the body.
102
103 Returns:
104 Mime type.
105 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500106 return 'application/octet-stream'
107
Joe Gregorio910b9b12012-06-12 09:36:30 -0400108 def size(self):
109 """Size of upload.
110
111 Returns:
112 Size of the body, or None of the size is unknown.
113 """
114 return None
115
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500116 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400117 """Whether this upload is resumable.
118
119 Returns:
120 True if resumable upload or False.
121 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500122 return False
123
Joe Gregorio910b9b12012-06-12 09:36:30 -0400124 def getbytes(self, begin, end):
125 """Get bytes from the media.
126
127 Args:
128 begin: int, offset from beginning of file.
129 length: int, number of bytes to read, starting at begin.
130
131 Returns:
132 A string of bytes read. May be shorter than length if EOF was reached
133 first.
134 """
135 raise NotImplementedError()
136
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500137 def _to_json(self, strip=None):
138 """Utility function for creating a JSON representation of a MediaUpload.
139
140 Args:
141 strip: array, An array of names of members to not include in the JSON.
142
143 Returns:
144 string, a JSON representation of this instance, suitable to pass to
145 from_json().
146 """
147 t = type(self)
148 d = copy.copy(self.__dict__)
149 if strip is not None:
150 for member in strip:
151 del d[member]
152 d['_class'] = t.__name__
153 d['_module'] = t.__module__
154 return simplejson.dumps(d)
155
156 def to_json(self):
157 """Create a JSON representation of an instance of MediaUpload.
158
159 Returns:
160 string, a JSON representation of this instance, suitable to pass to
161 from_json().
162 """
163 return self._to_json()
164
165 @classmethod
166 def new_from_json(cls, s):
167 """Utility class method to instantiate a MediaUpload subclass from a JSON
168 representation produced by to_json().
169
170 Args:
171 s: string, JSON from to_json().
172
173 Returns:
174 An instance of the subclass of MediaUpload that was serialized with
175 to_json().
176 """
177 data = simplejson.loads(s)
178 # Find and call the right classmethod from_json() to restore the object.
179 module = data['_module']
180 m = __import__(module, fromlist=module.split('.')[:-1])
181 kls = getattr(m, data['_class'])
182 from_json = getattr(kls, 'from_json')
183 return from_json(s)
184
Joe Gregorio66f57522011-11-30 11:00:00 -0500185
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500186class MediaFileUpload(MediaUpload):
187 """A MediaUpload for a file.
188
189 Construct a MediaFileUpload and pass as the media_body parameter of the
190 method. For example, if we had a service that allowed uploading images:
191
192
Joe Gregorio910b9b12012-06-12 09:36:30 -0400193 media = MediaFileUpload('smiley.png', mimetype='image/png',
194 chunksize=1024*1024, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500195 service.objects().insert(
196 bucket=buckets['items'][0]['id'],
197 name='smiley.png',
198 media_body=media).execute()
199 """
200
Joe Gregorio910b9b12012-06-12 09:36:30 -0400201 def __init__(self, filename, mimetype=None, chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500202 """Constructor.
203
204 Args:
205 filename: string, Name of the file.
206 mimetype: string, Mime-type of the file. If None then a mime-type will be
207 guessed from the file extension.
208 chunksize: int, File will be uploaded in chunks of this many bytes. Only
209 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500210 resumable: bool, True if this is a resumable upload. False means upload
211 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500212 """
213 self._filename = filename
214 self._size = os.path.getsize(filename)
215 self._fd = None
216 if mimetype is None:
217 (mimetype, encoding) = mimetypes.guess_type(filename)
218 self._mimetype = mimetype
219 self._chunksize = chunksize
220 self._resumable = resumable
221
Joe Gregorio910b9b12012-06-12 09:36:30 -0400222 def chunksize(self):
223 """Chunk size for resumable uploads.
224
225 Returns:
226 Chunk size in bytes.
227 """
228 return self._chunksize
229
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500230 def mimetype(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400231 """Mime type of the body.
232
233 Returns:
234 Mime type.
235 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500236 return self._mimetype
237
238 def size(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400239 """Size of upload.
240
241 Returns:
242 Size of the body, or None of the size is unknown.
243 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500244 return self._size
245
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500246 def resumable(self):
Joe Gregorio910b9b12012-06-12 09:36:30 -0400247 """Whether this upload is resumable.
248
249 Returns:
250 True if resumable upload or False.
251 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500252 return self._resumable
253
254 def getbytes(self, begin, length):
255 """Get bytes from the media.
256
257 Args:
258 begin: int, offset from beginning of file.
259 length: int, number of bytes to read, starting at begin.
260
261 Returns:
262 A string of bytes read. May be shorted than length if EOF was reached
263 first.
264 """
265 if self._fd is None:
266 self._fd = open(self._filename, 'rb')
267 self._fd.seek(begin)
268 return self._fd.read(length)
269
270 def to_json(self):
271 """Creating a JSON representation of an instance of Credentials.
272
273 Returns:
274 string, a JSON representation of this instance, suitable to pass to
275 from_json().
276 """
277 return self._to_json(['_fd'])
278
279 @staticmethod
280 def from_json(s):
281 d = simplejson.loads(s)
282 return MediaFileUpload(
283 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
284
285
Joe Gregorio910b9b12012-06-12 09:36:30 -0400286class MediaIoBaseUpload(MediaUpload):
287 """A MediaUpload for a io.Base objects.
288
289 Note that the Python file object is compatible with io.Base and can be used
290 with this class also.
291
Joe Gregorio910b9b12012-06-12 09:36:30 -0400292 fh = io.BytesIO('...Some data to upload...')
293 media = MediaIoBaseUpload(fh, mimetype='image/png',
294 chunksize=1024*1024, resumable=True)
295 service.objects().insert(
296 bucket='a_bucket_id',
297 name='smiley.png',
298 media_body=media).execute()
299 """
300
301 def __init__(self, fh, mimetype, chunksize=DEFAULT_CHUNK_SIZE,
302 resumable=False):
303 """Constructor.
304
305 Args:
Joe Gregorio44454e42012-06-15 08:38:53 -0400306 fh: io.Base or file object, The source of the bytes to upload. MUST be
307 opened in blocking mode, do not use streams opened in non-blocking mode.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400308 mimetype: string, Mime-type of the file. If None then a mime-type will be
309 guessed from the file extension.
310 chunksize: int, File will be uploaded in chunks of this many bytes. Only
311 used if resumable=True.
312 resumable: bool, True if this is a resumable upload. False means upload
313 in a single request.
314 """
315 self._fh = fh
316 self._mimetype = mimetype
317 self._chunksize = chunksize
318 self._resumable = resumable
319 self._size = None
320 try:
321 if hasattr(self._fh, 'fileno'):
322 fileno = self._fh.fileno()
Joe Gregorio44454e42012-06-15 08:38:53 -0400323
324 # Pipes and such show up as 0 length files.
325 size = os.fstat(fileno).st_size
326 if size:
327 self._size = os.fstat(fileno).st_size
Joe Gregorio910b9b12012-06-12 09:36:30 -0400328 except IOError:
329 pass
330
331 def chunksize(self):
332 """Chunk size for resumable uploads.
333
334 Returns:
335 Chunk size in bytes.
336 """
337 return self._chunksize
338
339 def mimetype(self):
340 """Mime type of the body.
341
342 Returns:
343 Mime type.
344 """
345 return self._mimetype
346
347 def size(self):
348 """Size of upload.
349
350 Returns:
351 Size of the body, or None of the size is unknown.
352 """
353 return self._size
354
355 def resumable(self):
356 """Whether this upload is resumable.
357
358 Returns:
359 True if resumable upload or False.
360 """
361 return self._resumable
362
363 def getbytes(self, begin, length):
364 """Get bytes from the media.
365
366 Args:
367 begin: int, offset from beginning of file.
368 length: int, number of bytes to read, starting at begin.
369
370 Returns:
371 A string of bytes read. May be shorted than length if EOF was reached
372 first.
373 """
374 self._fh.seek(begin)
375 return self._fh.read(length)
376
377 def to_json(self):
378 """This upload type is not serializable."""
379 raise NotImplementedError('MediaIoBaseUpload is not serializable.')
380
381
Ali Afshar6f11ea12012-02-07 10:32:14 -0500382class MediaInMemoryUpload(MediaUpload):
383 """MediaUpload for a chunk of bytes.
384
385 Construct a MediaFileUpload and pass as the media_body parameter of the
386 method. For example, if we had a service that allowed plain text:
387 """
388
389 def __init__(self, body, mimetype='application/octet-stream',
Joe Gregorio910b9b12012-06-12 09:36:30 -0400390 chunksize=DEFAULT_CHUNK_SIZE, resumable=False):
Ali Afshar6f11ea12012-02-07 10:32:14 -0500391 """Create a new MediaBytesUpload.
392
393 Args:
394 body: string, Bytes of body content.
395 mimetype: string, Mime-type of the file or default of
396 'application/octet-stream'.
397 chunksize: int, File will be uploaded in chunks of this many bytes. Only
398 used if resumable=True.
399 resumable: bool, True if this is a resumable upload. False means upload
400 in a single request.
401 """
402 self._body = body
403 self._mimetype = mimetype
404 self._resumable = resumable
405 self._chunksize = chunksize
406
407 def chunksize(self):
408 """Chunk size for resumable uploads.
409
410 Returns:
411 Chunk size in bytes.
412 """
413 return self._chunksize
414
415 def mimetype(self):
416 """Mime type of the body.
417
418 Returns:
419 Mime type.
420 """
421 return self._mimetype
422
423 def size(self):
424 """Size of upload.
425
426 Returns:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400427 Size of the body, or None of the size is unknown.
Ali Afshar6f11ea12012-02-07 10:32:14 -0500428 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400429 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500430
431 def resumable(self):
432 """Whether this upload is resumable.
433
434 Returns:
435 True if resumable upload or False.
436 """
437 return self._resumable
438
439 def getbytes(self, begin, length):
440 """Get bytes from the media.
441
442 Args:
443 begin: int, offset from beginning of file.
444 length: int, number of bytes to read, starting at begin.
445
446 Returns:
447 A string of bytes read. May be shorter than length if EOF was reached
448 first.
449 """
450 return self._body[begin:begin + length]
451
452 def to_json(self):
453 """Create a JSON representation of a MediaInMemoryUpload.
454
455 Returns:
456 string, a JSON representation of this instance, suitable to pass to
457 from_json().
458 """
459 t = type(self)
460 d = copy.copy(self.__dict__)
461 del d['_body']
462 d['_class'] = t.__name__
463 d['_module'] = t.__module__
464 d['_b64body'] = base64.b64encode(self._body)
465 return simplejson.dumps(d)
466
467 @staticmethod
468 def from_json(s):
469 d = simplejson.loads(s)
470 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
471 d['_mimetype'], d['_chunksize'],
472 d['_resumable'])
473
474
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400475class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500476 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400477
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500478 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500479 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500480 body=None,
481 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500482 methodId=None,
483 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500484 """Constructor for an HttpRequest.
485
Joe Gregorioaf276d22010-12-09 14:26:58 -0500486 Args:
487 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500488 postproc: callable, called on the HTTP response and content to transform
489 it into a data object before returning, or raising an exception
490 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500491 uri: string, the absolute URI to send the request to
492 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500493 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500494 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500495 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500496 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500497 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400498 self.uri = uri
499 self.method = method
500 self.body = body
501 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500502 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400503 self.http = http
504 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500505 self.resumable = resumable
Joe Gregorio910b9b12012-06-12 09:36:30 -0400506 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500507
Joe Gregorio66f57522011-11-30 11:00:00 -0500508 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500509 major, minor, params = mimeparse.parse_mime_type(
510 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500511
Joe Gregorio945be3e2012-01-27 17:01:06 -0500512 # The size of the non-media part of the request.
513 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500514
515 # The resumable URI to send chunks to.
516 self.resumable_uri = None
517
518 # The bytes that have been uploaded.
519 self.resumable_progress = 0
520
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400521 def execute(self, http=None):
522 """Execute the request.
523
Joe Gregorioaf276d22010-12-09 14:26:58 -0500524 Args:
525 http: httplib2.Http, an http object to be used in place of the
526 one the HttpRequest request object was constructed with.
527
528 Returns:
529 A deserialized object model of the response body as determined
530 by the postproc.
531
532 Raises:
533 apiclient.errors.HttpError if the response was not a 2xx.
534 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400535 """
536 if http is None:
537 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500538 if self.resumable:
539 body = None
540 while body is None:
541 _, body = self.next_chunk(http)
542 return body
543 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500544 if 'content-length' not in self.headers:
545 self.headers['content-length'] = str(self.body_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500546 resp, content = http.request(self.uri, self.method,
547 body=self.body,
548 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500549
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500550 if resp.status >= 300:
551 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400552 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500553
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500554 def next_chunk(self, http=None):
555 """Execute the next step of a resumable upload.
556
Joe Gregorio66f57522011-11-30 11:00:00 -0500557 Can only be used if the method being executed supports media uploads and
558 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500559
560 Example:
561
Joe Gregorio66f57522011-11-30 11:00:00 -0500562 media = MediaFileUpload('smiley.png', mimetype='image/png',
563 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500564 request = service.objects().insert(
565 bucket=buckets['items'][0]['id'],
566 name='smiley.png',
567 media_body=media)
568
569 response = None
570 while response is None:
571 status, response = request.next_chunk()
572 if status:
573 print "Upload %d%% complete." % int(status.progress() * 100)
574
575
576 Returns:
577 (status, body): (ResumableMediaStatus, object)
578 The body will be None until the resumable media is fully uploaded.
Joe Gregorio910b9b12012-06-12 09:36:30 -0400579
580 Raises:
581 apiclient.errors.HttpError if the response was not a 2xx.
582 httplib2.Error if a transport error has occured.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500583 """
584 if http is None:
585 http = self.http
586
Joe Gregorio910b9b12012-06-12 09:36:30 -0400587 if self.resumable.size() is None:
588 size = '*'
589 else:
590 size = str(self.resumable.size())
591
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500592 if self.resumable_uri is None:
593 start_headers = copy.copy(self.headers)
594 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
Joe Gregorio910b9b12012-06-12 09:36:30 -0400595 if size != '*':
596 start_headers['X-Upload-Content-Length'] = size
Joe Gregorio945be3e2012-01-27 17:01:06 -0500597 start_headers['content-length'] = str(self.body_size)
598
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500599 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500600 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500601 headers=start_headers)
602 if resp.status == 200 and 'location' in resp:
603 self.resumable_uri = resp['location']
604 else:
605 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregorio910b9b12012-06-12 09:36:30 -0400606 elif self._in_error_state:
607 # If we are in an error state then query the server for current state of
608 # the upload by sending an empty PUT and reading the 'range' header in
609 # the response.
610 headers = {
611 'Content-Range': 'bytes */%s' % size,
612 'content-length': '0'
613 }
614 resp, content = http.request(self.resumable_uri, 'PUT',
615 headers=headers)
616 status, body = self._process_response(resp, content)
617 if body:
618 # The upload was complete.
619 return (status, body)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500620
Joe Gregorio910b9b12012-06-12 09:36:30 -0400621 data = self.resumable.getbytes(
622 self.resumable_progress, self.resumable.chunksize())
Joe Gregorio44454e42012-06-15 08:38:53 -0400623
624 # A short read implies that we are at EOF, so finish the upload.
625 if len(data) < self.resumable.chunksize():
626 size = str(self.resumable_progress + len(data))
627
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500628 headers = {
Joe Gregorio910b9b12012-06-12 09:36:30 -0400629 'Content-Range': 'bytes %d-%d/%s' % (
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500630 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio910b9b12012-06-12 09:36:30 -0400631 size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500632 }
Joe Gregorio910b9b12012-06-12 09:36:30 -0400633 try:
634 resp, content = http.request(self.resumable_uri, 'PUT',
635 body=data,
636 headers=headers)
637 except:
638 self._in_error_state = True
639 raise
640
641 return self._process_response(resp, content)
642
643 def _process_response(self, resp, content):
644 """Process the response from a single chunk upload.
645
646 Args:
647 resp: httplib2.Response, the response object.
648 content: string, the content of the response.
649
650 Returns:
651 (status, body): (ResumableMediaStatus, object)
652 The body will be None until the resumable media is fully uploaded.
653
654 Raises:
655 apiclient.errors.HttpError if the response was not a 2xx or a 308.
656 """
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500657 if resp.status in [200, 201]:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400658 self._in_error_state = False
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500659 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500660 elif resp.status == 308:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400661 self._in_error_state = False
Joe Gregorio66f57522011-11-30 11:00:00 -0500662 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500663 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500664 if 'location' in resp:
665 self.resumable_uri = resp['location']
666 else:
Joe Gregorio910b9b12012-06-12 09:36:30 -0400667 self._in_error_state = True
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500668 raise HttpError(resp, content, self.uri)
669
Joe Gregorio945be3e2012-01-27 17:01:06 -0500670 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
671 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500672
673 def to_json(self):
674 """Returns a JSON representation of the HttpRequest."""
675 d = copy.copy(self.__dict__)
676 if d['resumable'] is not None:
677 d['resumable'] = self.resumable.to_json()
678 del d['http']
679 del d['postproc']
Joe Gregorio910b9b12012-06-12 09:36:30 -0400680
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500681 return simplejson.dumps(d)
682
683 @staticmethod
684 def from_json(s, http, postproc):
685 """Returns an HttpRequest populated with info from a JSON object."""
686 d = simplejson.loads(s)
687 if d['resumable'] is not None:
688 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
689 return HttpRequest(
690 http,
691 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500692 uri=d['uri'],
693 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500694 body=d['body'],
695 headers=d['headers'],
696 methodId=d['methodId'],
697 resumable=d['resumable'])
698
Joe Gregorioaf276d22010-12-09 14:26:58 -0500699
Joe Gregorio66f57522011-11-30 11:00:00 -0500700class BatchHttpRequest(object):
701 """Batches multiple HttpRequest objects into a single HTTP request."""
702
703 def __init__(self, callback=None, batch_uri=None):
704 """Constructor for a BatchHttpRequest.
705
706 Args:
707 callback: callable, A callback to be called for each response, of the
708 form callback(id, response). The first parameter is the request id, and
709 the second is the deserialized response object.
710 batch_uri: string, URI to send batch requests to.
711 """
712 if batch_uri is None:
713 batch_uri = 'https://www.googleapis.com/batch'
714 self._batch_uri = batch_uri
715
716 # Global callback to be called for each individual response in the batch.
717 self._callback = callback
718
Joe Gregorio654f4a22012-02-09 14:15:44 -0500719 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500720 self._requests = {}
721
Joe Gregorio654f4a22012-02-09 14:15:44 -0500722 # A map from id to callback.
723 self._callbacks = {}
724
Joe Gregorio66f57522011-11-30 11:00:00 -0500725 # List of request ids, in the order in which they were added.
726 self._order = []
727
728 # The last auto generated id.
729 self._last_auto_id = 0
730
731 # Unique ID on which to base the Content-ID headers.
732 self._base_id = None
733
Joe Gregorio654f4a22012-02-09 14:15:44 -0500734 # A map from request id to (headers, content) response pairs
735 self._responses = {}
736
737 # A map of id(Credentials) that have been refreshed.
738 self._refreshed_credentials = {}
739
740 def _refresh_and_apply_credentials(self, request, http):
741 """Refresh the credentials and apply to the request.
742
743 Args:
744 request: HttpRequest, the request.
745 http: httplib2.Http, the global http object for the batch.
746 """
747 # For the credentials to refresh, but only once per refresh_token
748 # If there is no http per the request then refresh the http passed in
749 # via execute()
750 creds = None
751 if request.http is not None and hasattr(request.http.request,
752 'credentials'):
753 creds = request.http.request.credentials
754 elif http is not None and hasattr(http.request, 'credentials'):
755 creds = http.request.credentials
756 if creds is not None:
757 if id(creds) not in self._refreshed_credentials:
758 creds.refresh(http)
759 self._refreshed_credentials[id(creds)] = 1
760
761 # Only apply the credentials if we are using the http object passed in,
762 # otherwise apply() will get called during _serialize_request().
763 if request.http is None or not hasattr(request.http.request,
764 'credentials'):
765 creds.apply(request.headers)
766
Joe Gregorio66f57522011-11-30 11:00:00 -0500767 def _id_to_header(self, id_):
768 """Convert an id to a Content-ID header value.
769
770 Args:
771 id_: string, identifier of individual request.
772
773 Returns:
774 A Content-ID header with the id_ encoded into it. A UUID is prepended to
775 the value because Content-ID headers are supposed to be universally
776 unique.
777 """
778 if self._base_id is None:
779 self._base_id = uuid.uuid4()
780
781 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
782
783 def _header_to_id(self, header):
784 """Convert a Content-ID header value to an id.
785
786 Presumes the Content-ID header conforms to the format that _id_to_header()
787 returns.
788
789 Args:
790 header: string, Content-ID header value.
791
792 Returns:
793 The extracted id value.
794
795 Raises:
796 BatchError if the header is not in the expected format.
797 """
798 if header[0] != '<' or header[-1] != '>':
799 raise BatchError("Invalid value for Content-ID: %s" % header)
800 if '+' not in header:
801 raise BatchError("Invalid value for Content-ID: %s" % header)
802 base, id_ = header[1:-1].rsplit('+', 1)
803
804 return urllib.unquote(id_)
805
806 def _serialize_request(self, request):
807 """Convert an HttpRequest object into a string.
808
809 Args:
810 request: HttpRequest, the request to serialize.
811
812 Returns:
813 The request as a string in application/http format.
814 """
815 # Construct status line
816 parsed = urlparse.urlparse(request.uri)
817 request_line = urlparse.urlunparse(
818 (None, None, parsed.path, parsed.params, parsed.query, None)
819 )
820 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500821 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500822 msg = MIMENonMultipart(major, minor)
823 headers = request.headers.copy()
824
Joe Gregorio654f4a22012-02-09 14:15:44 -0500825 if request.http is not None and hasattr(request.http.request,
826 'credentials'):
827 request.http.request.credentials.apply(headers)
828
Joe Gregorio66f57522011-11-30 11:00:00 -0500829 # MIMENonMultipart adds its own Content-Type header.
830 if 'content-type' in headers:
831 del headers['content-type']
832
833 for key, value in headers.iteritems():
834 msg[key] = value
835 msg['Host'] = parsed.netloc
836 msg.set_unixfrom(None)
837
838 if request.body is not None:
839 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500840 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500841
Joe Gregorio654f4a22012-02-09 14:15:44 -0500842 # Serialize the mime message.
843 fp = StringIO.StringIO()
844 # maxheaderlen=0 means don't line wrap headers.
845 g = Generator(fp, maxheaderlen=0)
846 g.flatten(msg, unixfrom=False)
847 body = fp.getvalue()
848
Joe Gregorio66f57522011-11-30 11:00:00 -0500849 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
850 if request.body is None:
851 body = body[:-2]
852
Joe Gregoriodd813822012-01-25 10:32:47 -0500853 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500854
855 def _deserialize_response(self, payload):
856 """Convert string into httplib2 response and content.
857
858 Args:
859 payload: string, headers and body as a string.
860
861 Returns:
862 A pair (resp, content) like would be returned from httplib2.request.
863 """
864 # Strip off the status line
865 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500866 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500867
868 # Parse the rest of the response
869 parser = FeedParser()
870 parser.feed(payload)
871 msg = parser.close()
872 msg['status'] = status
873
874 # Create httplib2.Response from the parsed headers.
875 resp = httplib2.Response(msg)
876 resp.reason = reason
877 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
878
879 content = payload.split('\r\n\r\n', 1)[1]
880
881 return resp, content
882
883 def _new_id(self):
884 """Create a new id.
885
886 Auto incrementing number that avoids conflicts with ids already used.
887
888 Returns:
889 string, a new unique id.
890 """
891 self._last_auto_id += 1
892 while str(self._last_auto_id) in self._requests:
893 self._last_auto_id += 1
894 return str(self._last_auto_id)
895
896 def add(self, request, callback=None, request_id=None):
897 """Add a new request.
898
899 Every callback added will be paired with a unique id, the request_id. That
900 unique id will be passed back to the callback when the response comes back
901 from the server. The default behavior is to have the library generate it's
902 own unique id. If the caller passes in a request_id then they must ensure
903 uniqueness for each request_id, and if they are not an exception is
904 raised. Callers should either supply all request_ids or nevery supply a
905 request id, to avoid such an error.
906
907 Args:
908 request: HttpRequest, Request to add to the batch.
909 callback: callable, A callback to be called for this response, of the
910 form callback(id, response). The first parameter is the request id, and
911 the second is the deserialized response object.
912 request_id: string, A unique id for the request. The id will be passed to
913 the callback with the response.
914
915 Returns:
916 None
917
918 Raises:
919 BatchError if a resumable request is added to a batch.
920 KeyError is the request_id is not unique.
921 """
922 if request_id is None:
923 request_id = self._new_id()
924 if request.resumable is not None:
925 raise BatchError("Resumable requests cannot be used in a batch request.")
926 if request_id in self._requests:
927 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500928 self._requests[request_id] = request
929 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -0500930 self._order.append(request_id)
931
Joe Gregorio654f4a22012-02-09 14:15:44 -0500932 def _execute(self, http, order, requests):
933 """Serialize batch request, send to server, process response.
934
935 Args:
936 http: httplib2.Http, an http object to be used to make the request with.
937 order: list, list of request ids in the order they were added to the
938 batch.
939 request: list, list of request objects to send.
940
941 Raises:
942 httplib2.Error if a transport error has occured.
943 apiclient.errors.BatchError if the response is the wrong format.
944 """
945 message = MIMEMultipart('mixed')
946 # Message should not write out it's own headers.
947 setattr(message, '_write_headers', lambda self: None)
948
949 # Add all the individual requests.
950 for request_id in order:
951 request = requests[request_id]
952
953 msg = MIMENonMultipart('application', 'http')
954 msg['Content-Transfer-Encoding'] = 'binary'
955 msg['Content-ID'] = self._id_to_header(request_id)
956
957 body = self._serialize_request(request)
958 msg.set_payload(body)
959 message.attach(msg)
960
961 body = message.as_string()
962
963 headers = {}
964 headers['content-type'] = ('multipart/mixed; '
965 'boundary="%s"') % message.get_boundary()
966
967 resp, content = http.request(self._batch_uri, 'POST', body=body,
968 headers=headers)
969
970 if resp.status >= 300:
971 raise HttpError(resp, content, self._batch_uri)
972
973 # Now break out the individual responses and store each one.
974 boundary, _ = content.split(None, 1)
975
976 # Prepend with a content-type header so FeedParser can handle it.
977 header = 'content-type: %s\r\n\r\n' % resp['content-type']
978 for_parser = header + content
979
980 parser = FeedParser()
981 parser.feed(for_parser)
982 mime_response = parser.close()
983
984 if not mime_response.is_multipart():
985 raise BatchError("Response not in multipart/mixed format.", resp,
986 content)
987
988 for part in mime_response.get_payload():
989 request_id = self._header_to_id(part['Content-ID'])
990 headers, content = self._deserialize_response(part.get_payload())
991 self._responses[request_id] = (headers, content)
992
Joe Gregorio66f57522011-11-30 11:00:00 -0500993 def execute(self, http=None):
994 """Execute all the requests as a single batched HTTP request.
995
996 Args:
997 http: httplib2.Http, an http object to be used in place of the one the
998 HttpRequest request object was constructed with. If one isn't supplied
999 then use a http object from the requests in this batch.
1000
1001 Returns:
1002 None
1003
1004 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -05001005 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -05001006 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -05001007 """
Joe Gregorio654f4a22012-02-09 14:15:44 -05001008
1009 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -05001010 if http is None:
1011 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001012 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001013 if request is not None:
1014 http = request.http
1015 break
Joe Gregorio654f4a22012-02-09 14:15:44 -05001016
Joe Gregorio66f57522011-11-30 11:00:00 -05001017 if http is None:
1018 raise ValueError("Missing a valid http object.")
1019
Joe Gregorio654f4a22012-02-09 14:15:44 -05001020 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001021
Joe Gregorio654f4a22012-02-09 14:15:44 -05001022 # Loop over all the requests and check for 401s. For each 401 request the
1023 # credentials should be refreshed and then sent again in a separate batch.
1024 redo_requests = {}
1025 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -05001026
Joe Gregorio66f57522011-11-30 11:00:00 -05001027 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001028 headers, content = self._responses[request_id]
1029 if headers['status'] == '401':
1030 redo_order.append(request_id)
1031 request = self._requests[request_id]
1032 self._refresh_and_apply_credentials(request, http)
1033 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -05001034
Joe Gregorio654f4a22012-02-09 14:15:44 -05001035 if redo_requests:
1036 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -05001037
Joe Gregorio654f4a22012-02-09 14:15:44 -05001038 # Now process all callbacks that are erroring, and raise an exception for
1039 # ones that return a non-2xx response? Or add extra parameter to callback
1040 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -05001041
Joe Gregorio654f4a22012-02-09 14:15:44 -05001042 for request_id in self._order:
1043 headers, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001044
Joe Gregorio654f4a22012-02-09 14:15:44 -05001045 request = self._requests[request_id]
1046 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -05001047
Joe Gregorio654f4a22012-02-09 14:15:44 -05001048 response = None
1049 exception = None
1050 try:
1051 r = httplib2.Response(headers)
1052 response = request.postproc(r, content)
1053 except HttpError, e:
1054 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -05001055
Joe Gregorio654f4a22012-02-09 14:15:44 -05001056 if callback is not None:
1057 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001058 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -05001059 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -05001060
1061
Joe Gregorioaf276d22010-12-09 14:26:58 -05001062class HttpRequestMock(object):
1063 """Mock of HttpRequest.
1064
1065 Do not construct directly, instead use RequestMockBuilder.
1066 """
1067
1068 def __init__(self, resp, content, postproc):
1069 """Constructor for HttpRequestMock
1070
1071 Args:
1072 resp: httplib2.Response, the response to emulate coming from the request
1073 content: string, the response body
1074 postproc: callable, the post processing function usually supplied by
1075 the model class. See model.JsonModel.response() as an example.
1076 """
1077 self.resp = resp
1078 self.content = content
1079 self.postproc = postproc
1080 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -05001081 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -05001082 if 'reason' in self.resp:
1083 self.resp.reason = self.resp['reason']
1084
1085 def execute(self, http=None):
1086 """Execute the request.
1087
1088 Same behavior as HttpRequest.execute(), but the response is
1089 mocked and not really from an HTTP request/response.
1090 """
1091 return self.postproc(self.resp, self.content)
1092
1093
1094class RequestMockBuilder(object):
1095 """A simple mock of HttpRequest
1096
1097 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -04001098 tuples of (httplib2.Response, content, opt_expected_body) that should be
1099 returned when that method is called. None may also be passed in for the
1100 httplib2.Response, in which case a 200 OK response will be generated.
1101 If an opt_expected_body (str or dict) is provided, it will be compared to
1102 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001103
1104 Example:
1105 response = '{"data": {"id": "tag:google.c...'
1106 requestBuilder = RequestMockBuilder(
1107 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001108 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -05001109 }
1110 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -05001111 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001112
1113 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -05001114 200 OK with an empty string as the response content or raise an excpetion
1115 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -04001116 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001117
1118 For more details see the project wiki.
1119 """
1120
Joe Gregorioa388ce32011-09-09 17:19:13 -04001121 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001122 """Constructor for RequestMockBuilder
1123
1124 The constructed object should be a callable object
1125 that can replace the class HttpResponse.
1126
1127 responses - A dictionary that maps methodIds into tuples
1128 of (httplib2.Response, content). The methodId
1129 comes from the 'rpcName' field in the discovery
1130 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -04001131 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1132 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -05001133 """
1134 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -04001135 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -05001136
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001137 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -05001138 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -05001139 """Implements the callable interface that discovery.build() expects
1140 of requestBuilder, which is to build an object compatible with
1141 HttpRequest.execute(). See that method for the description of the
1142 parameters and the expected response.
1143 """
1144 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -04001145 response = self.responses[methodId]
1146 resp, content = response[:2]
1147 if len(response) > 2:
1148 # Test the body against the supplied expected_body.
1149 expected_body = response[2]
1150 if bool(expected_body) != bool(body):
1151 # Not expecting a body and provided one
1152 # or expecting a body and not provided one.
1153 raise UnexpectedBodyError(expected_body, body)
1154 if isinstance(expected_body, str):
1155 expected_body = simplejson.loads(expected_body)
1156 body = simplejson.loads(body)
1157 if body != expected_body:
1158 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001159 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -04001160 elif self.check_unexpected:
1161 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001162 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -05001163 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -05001164 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001165
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001166
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001167class HttpMock(object):
1168 """Mock of httplib2.Http"""
1169
Joe Gregorioec343652011-02-16 16:52:51 -05001170 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001171 """
1172 Args:
1173 filename: string, absolute filename to read response from
1174 headers: dict, header to return with response
1175 """
Joe Gregorioec343652011-02-16 16:52:51 -05001176 if headers is None:
1177 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001178 f = file(filename, 'r')
1179 self.data = f.read()
1180 f.close()
1181 self.headers = headers
1182
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001183 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -05001184 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -05001185 body=None,
1186 headers=None,
1187 redirections=1,
1188 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -05001189 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -05001190
1191
1192class HttpMockSequence(object):
1193 """Mock of httplib2.Http
1194
1195 Mocks a sequence of calls to request returning different responses for each
1196 call. Create an instance initialized with the desired response headers
1197 and content and then use as if an httplib2.Http instance.
1198
1199 http = HttpMockSequence([
1200 ({'status': '401'}, ''),
1201 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1202 ({'status': '200'}, 'echo_request_headers'),
1203 ])
1204 resp, content = http.request("http://examples.com")
1205
1206 There are special values you can pass in for content to trigger
1207 behavours that are helpful in testing.
1208
1209 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001210 'echo_request_headers_as_json' means return the request headers in
1211 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001212 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001213 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001214 """
1215
1216 def __init__(self, iterable):
1217 """
1218 Args:
1219 iterable: iterable, a sequence of pairs of (headers, body)
1220 """
1221 self._iterable = iterable
1222
1223 def request(self, uri,
1224 method='GET',
1225 body=None,
1226 headers=None,
1227 redirections=1,
1228 connection_type=None):
1229 resp, content = self._iterable.pop(0)
1230 if content == 'echo_request_headers':
1231 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001232 elif content == 'echo_request_headers_as_json':
1233 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001234 elif content == 'echo_request_body':
1235 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001236 elif content == 'echo_request_uri':
1237 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001238 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001239
1240
1241def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001242 """Set the user-agent on every request.
1243
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001244 Args:
1245 http - An instance of httplib2.Http
1246 or something that acts like it.
1247 user_agent: string, the value for the user-agent header.
1248
1249 Returns:
1250 A modified instance of http that was passed in.
1251
1252 Example:
1253
1254 h = httplib2.Http()
1255 h = set_user_agent(h, "my-app-name/6.0")
1256
1257 Most of the time the user-agent will be set doing auth, this is for the rare
1258 cases where you are accessing an unauthenticated endpoint.
1259 """
1260 request_orig = http.request
1261
1262 # The closure that will replace 'httplib2.Http.request'.
1263 def new_request(uri, method='GET', body=None, headers=None,
1264 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1265 connection_type=None):
1266 """Modify the request headers to add the user-agent."""
1267 if headers is None:
1268 headers = {}
1269 if 'user-agent' in headers:
1270 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1271 else:
1272 headers['user-agent'] = user_agent
1273 resp, content = request_orig(uri, method, body, headers,
1274 redirections, connection_type)
1275 return resp, content
1276
1277 http.request = new_request
1278 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001279
1280
1281def tunnel_patch(http):
1282 """Tunnel PATCH requests over POST.
1283 Args:
1284 http - An instance of httplib2.Http
1285 or something that acts like it.
1286
1287 Returns:
1288 A modified instance of http that was passed in.
1289
1290 Example:
1291
1292 h = httplib2.Http()
1293 h = tunnel_patch(h, "my-app-name/6.0")
1294
1295 Useful if you are running on a platform that doesn't support PATCH.
1296 Apply this last if you are using OAuth 1.0, as changing the method
1297 will result in a different signature.
1298 """
1299 request_orig = http.request
1300
1301 # The closure that will replace 'httplib2.Http.request'.
1302 def new_request(uri, method='GET', body=None, headers=None,
1303 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1304 connection_type=None):
1305 """Modify the request headers to add the user-agent."""
1306 if headers is None:
1307 headers = {}
1308 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001309 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001310 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001311 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001312 headers['x-http-method-override'] = "PATCH"
1313 method = 'POST'
1314 resp, content = request_orig(uri, method, body, headers,
1315 redirections, connection_type)
1316 return resp, content
1317
1318 http.request = new_request
1319 return http