blob: 94eb266750998ed9997ea42e97ea79aebb12a711 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# Copyright (C) 2010 Google Inc.
2#
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__all__ = [
Joe Gregoriocb8103d2011-02-11 23:20:52 -050024 'HttpRequest', 'RequestMockBuilder', 'HttpMock'
Joe Gregoriof4153422011-03-18 22:45:18 -040025 'set_user_agent', 'tunnel_patch'
Joe Gregorioaf276d22010-12-09 14:26:58 -050026 ]
27
Joe Gregorio66f57522011-11-30 11:00:00 -050028import StringIO
Ali Afshar6f11ea12012-02-07 10:32:14 -050029import base64
Joe Gregoriod0bd3882011-11-22 09:49:47 -050030import copy
Joe Gregorio66f57522011-11-30 11:00:00 -050031import gzip
Joe Gregorioc6722462010-12-20 14:29:28 -050032import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050033import mimeparse
34import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050035import os
36import urllib
37import urlparse
38import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050039
Joe Gregorio66f57522011-11-30 11:00:00 -050040from email.mime.multipart import MIMEMultipart
41from email.mime.nonmultipart import MIMENonMultipart
42from email.parser import FeedParser
43from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050044from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050045from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040046from errors import UnexpectedBodyError
47from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050048from model import JsonModel
Joe Gregorio549230c2012-01-11 10:38:05 -050049from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040050
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.
60 total_size: int, total bytes in complete upload.
61 """
62 self.resumable_progress = resumable_progress
63 self.total_size = total_size
64
65 def progress(self):
66 """Percent of upload completed, as a float."""
Joe Gregorio66f57522011-11-30 11:00:00 -050067 return float(self.resumable_progress) / float(self.total_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -050068
69
70class MediaUpload(object):
71 """Describes a media object to upload.
72
73 Base class that defines the interface of MediaUpload subclasses.
74 """
75
76 def getbytes(self, begin, end):
77 raise NotImplementedError()
78
79 def size(self):
80 raise NotImplementedError()
81
82 def chunksize(self):
83 raise NotImplementedError()
84
85 def mimetype(self):
86 return 'application/octet-stream'
87
88 def resumable(self):
89 return False
90
91 def _to_json(self, strip=None):
92 """Utility function for creating a JSON representation of a MediaUpload.
93
94 Args:
95 strip: array, An array of names of members to not include in the JSON.
96
97 Returns:
98 string, a JSON representation of this instance, suitable to pass to
99 from_json().
100 """
101 t = type(self)
102 d = copy.copy(self.__dict__)
103 if strip is not None:
104 for member in strip:
105 del d[member]
106 d['_class'] = t.__name__
107 d['_module'] = t.__module__
108 return simplejson.dumps(d)
109
110 def to_json(self):
111 """Create a JSON representation of an instance of MediaUpload.
112
113 Returns:
114 string, a JSON representation of this instance, suitable to pass to
115 from_json().
116 """
117 return self._to_json()
118
119 @classmethod
120 def new_from_json(cls, s):
121 """Utility class method to instantiate a MediaUpload subclass from a JSON
122 representation produced by to_json().
123
124 Args:
125 s: string, JSON from to_json().
126
127 Returns:
128 An instance of the subclass of MediaUpload that was serialized with
129 to_json().
130 """
131 data = simplejson.loads(s)
132 # Find and call the right classmethod from_json() to restore the object.
133 module = data['_module']
134 m = __import__(module, fromlist=module.split('.')[:-1])
135 kls = getattr(m, data['_class'])
136 from_json = getattr(kls, 'from_json')
137 return from_json(s)
138
Joe Gregorio66f57522011-11-30 11:00:00 -0500139
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500140class MediaFileUpload(MediaUpload):
141 """A MediaUpload for a file.
142
143 Construct a MediaFileUpload and pass as the media_body parameter of the
144 method. For example, if we had a service that allowed uploading images:
145
146
147 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
148 resumable=True)
149 service.objects().insert(
150 bucket=buckets['items'][0]['id'],
151 name='smiley.png',
152 media_body=media).execute()
153 """
154
Joe Gregorio945be3e2012-01-27 17:01:06 -0500155 def __init__(self, filename, mimetype=None, chunksize=256*1024, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500156 """Constructor.
157
158 Args:
159 filename: string, Name of the file.
160 mimetype: string, Mime-type of the file. If None then a mime-type will be
161 guessed from the file extension.
162 chunksize: int, File will be uploaded in chunks of this many bytes. Only
163 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500164 resumable: bool, True if this is a resumable upload. False means upload
165 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500166 """
167 self._filename = filename
168 self._size = os.path.getsize(filename)
169 self._fd = None
170 if mimetype is None:
171 (mimetype, encoding) = mimetypes.guess_type(filename)
172 self._mimetype = mimetype
173 self._chunksize = chunksize
174 self._resumable = resumable
175
176 def mimetype(self):
177 return self._mimetype
178
179 def size(self):
180 return self._size
181
182 def chunksize(self):
183 return self._chunksize
184
185 def resumable(self):
186 return self._resumable
187
188 def getbytes(self, begin, length):
189 """Get bytes from the media.
190
191 Args:
192 begin: int, offset from beginning of file.
193 length: int, number of bytes to read, starting at begin.
194
195 Returns:
196 A string of bytes read. May be shorted than length if EOF was reached
197 first.
198 """
199 if self._fd is None:
200 self._fd = open(self._filename, 'rb')
201 self._fd.seek(begin)
202 return self._fd.read(length)
203
204 def to_json(self):
205 """Creating a JSON representation of an instance of Credentials.
206
207 Returns:
208 string, a JSON representation of this instance, suitable to pass to
209 from_json().
210 """
211 return self._to_json(['_fd'])
212
213 @staticmethod
214 def from_json(s):
215 d = simplejson.loads(s)
216 return MediaFileUpload(
217 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
218
219
Ali Afshar6f11ea12012-02-07 10:32:14 -0500220class MediaInMemoryUpload(MediaUpload):
221 """MediaUpload for a chunk of bytes.
222
223 Construct a MediaFileUpload and pass as the media_body parameter of the
224 method. For example, if we had a service that allowed plain text:
225 """
226
227 def __init__(self, body, mimetype='application/octet-stream',
228 chunksize=256*1024, resumable=False):
229 """Create a new MediaBytesUpload.
230
231 Args:
232 body: string, Bytes of body content.
233 mimetype: string, Mime-type of the file or default of
234 'application/octet-stream'.
235 chunksize: int, File will be uploaded in chunks of this many bytes. Only
236 used if resumable=True.
237 resumable: bool, True if this is a resumable upload. False means upload
238 in a single request.
239 """
240 self._body = body
241 self._mimetype = mimetype
242 self._resumable = resumable
243 self._chunksize = chunksize
244
245 def chunksize(self):
246 """Chunk size for resumable uploads.
247
248 Returns:
249 Chunk size in bytes.
250 """
251 return self._chunksize
252
253 def mimetype(self):
254 """Mime type of the body.
255
256 Returns:
257 Mime type.
258 """
259 return self._mimetype
260
261 def size(self):
262 """Size of upload.
263
264 Returns:
265 Size of the body.
266 """
267 return len(self.body)
268
269 def resumable(self):
270 """Whether this upload is resumable.
271
272 Returns:
273 True if resumable upload or False.
274 """
275 return self._resumable
276
277 def getbytes(self, begin, length):
278 """Get bytes from the media.
279
280 Args:
281 begin: int, offset from beginning of file.
282 length: int, number of bytes to read, starting at begin.
283
284 Returns:
285 A string of bytes read. May be shorter than length if EOF was reached
286 first.
287 """
288 return self._body[begin:begin + length]
289
290 def to_json(self):
291 """Create a JSON representation of a MediaInMemoryUpload.
292
293 Returns:
294 string, a JSON representation of this instance, suitable to pass to
295 from_json().
296 """
297 t = type(self)
298 d = copy.copy(self.__dict__)
299 del d['_body']
300 d['_class'] = t.__name__
301 d['_module'] = t.__module__
302 d['_b64body'] = base64.b64encode(self._body)
303 return simplejson.dumps(d)
304
305 @staticmethod
306 def from_json(s):
307 d = simplejson.loads(s)
308 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
309 d['_mimetype'], d['_chunksize'],
310 d['_resumable'])
311
312
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400313class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500314 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400315
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500316 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500317 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500318 body=None,
319 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500320 methodId=None,
321 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500322 """Constructor for an HttpRequest.
323
Joe Gregorioaf276d22010-12-09 14:26:58 -0500324 Args:
325 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500326 postproc: callable, called on the HTTP response and content to transform
327 it into a data object before returning, or raising an exception
328 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500329 uri: string, the absolute URI to send the request to
330 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500331 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500332 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500333 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500334 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500335 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400336 self.uri = uri
337 self.method = method
338 self.body = body
339 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500340 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400341 self.http = http
342 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500343 self.resumable = resumable
344
Joe Gregorio66f57522011-11-30 11:00:00 -0500345 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500346 major, minor, params = mimeparse.parse_mime_type(
347 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500348
Joe Gregorio945be3e2012-01-27 17:01:06 -0500349 # The size of the non-media part of the request.
350 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500351
352 # The resumable URI to send chunks to.
353 self.resumable_uri = None
354
355 # The bytes that have been uploaded.
356 self.resumable_progress = 0
357
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400358 def execute(self, http=None):
359 """Execute the request.
360
Joe Gregorioaf276d22010-12-09 14:26:58 -0500361 Args:
362 http: httplib2.Http, an http object to be used in place of the
363 one the HttpRequest request object was constructed with.
364
365 Returns:
366 A deserialized object model of the response body as determined
367 by the postproc.
368
369 Raises:
370 apiclient.errors.HttpError if the response was not a 2xx.
371 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400372 """
373 if http is None:
374 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500375 if self.resumable:
376 body = None
377 while body is None:
378 _, body = self.next_chunk(http)
379 return body
380 else:
381 resp, content = http.request(self.uri, self.method,
382 body=self.body,
383 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500384
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500385 if resp.status >= 300:
386 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400387 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500388
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500389 def next_chunk(self, http=None):
390 """Execute the next step of a resumable upload.
391
Joe Gregorio66f57522011-11-30 11:00:00 -0500392 Can only be used if the method being executed supports media uploads and
393 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500394
395 Example:
396
Joe Gregorio66f57522011-11-30 11:00:00 -0500397 media = MediaFileUpload('smiley.png', mimetype='image/png',
398 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500399 request = service.objects().insert(
400 bucket=buckets['items'][0]['id'],
401 name='smiley.png',
402 media_body=media)
403
404 response = None
405 while response is None:
406 status, response = request.next_chunk()
407 if status:
408 print "Upload %d%% complete." % int(status.progress() * 100)
409
410
411 Returns:
412 (status, body): (ResumableMediaStatus, object)
413 The body will be None until the resumable media is fully uploaded.
414 """
415 if http is None:
416 http = self.http
417
418 if self.resumable_uri is None:
419 start_headers = copy.copy(self.headers)
420 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
421 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
Joe Gregorio945be3e2012-01-27 17:01:06 -0500422 start_headers['content-length'] = str(self.body_size)
423
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500424 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500425 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500426 headers=start_headers)
427 if resp.status == 200 and 'location' in resp:
428 self.resumable_uri = resp['location']
429 else:
430 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500431
Joe Gregorio945be3e2012-01-27 17:01:06 -0500432 data = self.resumable.getbytes(self.resumable_progress,
433 self.resumable.chunksize())
434
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500435 headers = {
436 'Content-Range': 'bytes %d-%d/%d' % (
437 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500438 self.resumable.size()),
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500439 }
440 resp, content = http.request(self.resumable_uri, 'PUT',
441 body=data,
442 headers=headers)
443 if resp.status in [200, 201]:
444 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500445 elif resp.status == 308:
Joe Gregorio66f57522011-11-30 11:00:00 -0500446 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500447 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500448 if 'location' in resp:
449 self.resumable_uri = resp['location']
450 else:
451 raise HttpError(resp, content, self.uri)
452
Joe Gregorio945be3e2012-01-27 17:01:06 -0500453 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
454 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500455
456 def to_json(self):
457 """Returns a JSON representation of the HttpRequest."""
458 d = copy.copy(self.__dict__)
459 if d['resumable'] is not None:
460 d['resumable'] = self.resumable.to_json()
461 del d['http']
462 del d['postproc']
463 return simplejson.dumps(d)
464
465 @staticmethod
466 def from_json(s, http, postproc):
467 """Returns an HttpRequest populated with info from a JSON object."""
468 d = simplejson.loads(s)
469 if d['resumable'] is not None:
470 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
471 return HttpRequest(
472 http,
473 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500474 uri=d['uri'],
475 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500476 body=d['body'],
477 headers=d['headers'],
478 methodId=d['methodId'],
479 resumable=d['resumable'])
480
Joe Gregorioaf276d22010-12-09 14:26:58 -0500481
Joe Gregorio66f57522011-11-30 11:00:00 -0500482class BatchHttpRequest(object):
483 """Batches multiple HttpRequest objects into a single HTTP request."""
484
485 def __init__(self, callback=None, batch_uri=None):
486 """Constructor for a BatchHttpRequest.
487
488 Args:
489 callback: callable, A callback to be called for each response, of the
490 form callback(id, response). The first parameter is the request id, and
491 the second is the deserialized response object.
492 batch_uri: string, URI to send batch requests to.
493 """
494 if batch_uri is None:
495 batch_uri = 'https://www.googleapis.com/batch'
496 self._batch_uri = batch_uri
497
498 # Global callback to be called for each individual response in the batch.
499 self._callback = callback
500
501 # A map from id to (request, callback) pairs.
502 self._requests = {}
503
504 # List of request ids, in the order in which they were added.
505 self._order = []
506
507 # The last auto generated id.
508 self._last_auto_id = 0
509
510 # Unique ID on which to base the Content-ID headers.
511 self._base_id = None
512
513 def _id_to_header(self, id_):
514 """Convert an id to a Content-ID header value.
515
516 Args:
517 id_: string, identifier of individual request.
518
519 Returns:
520 A Content-ID header with the id_ encoded into it. A UUID is prepended to
521 the value because Content-ID headers are supposed to be universally
522 unique.
523 """
524 if self._base_id is None:
525 self._base_id = uuid.uuid4()
526
527 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
528
529 def _header_to_id(self, header):
530 """Convert a Content-ID header value to an id.
531
532 Presumes the Content-ID header conforms to the format that _id_to_header()
533 returns.
534
535 Args:
536 header: string, Content-ID header value.
537
538 Returns:
539 The extracted id value.
540
541 Raises:
542 BatchError if the header is not in the expected format.
543 """
544 if header[0] != '<' or header[-1] != '>':
545 raise BatchError("Invalid value for Content-ID: %s" % header)
546 if '+' not in header:
547 raise BatchError("Invalid value for Content-ID: %s" % header)
548 base, id_ = header[1:-1].rsplit('+', 1)
549
550 return urllib.unquote(id_)
551
552 def _serialize_request(self, request):
553 """Convert an HttpRequest object into a string.
554
555 Args:
556 request: HttpRequest, the request to serialize.
557
558 Returns:
559 The request as a string in application/http format.
560 """
561 # Construct status line
562 parsed = urlparse.urlparse(request.uri)
563 request_line = urlparse.urlunparse(
564 (None, None, parsed.path, parsed.params, parsed.query, None)
565 )
566 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500567 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500568 msg = MIMENonMultipart(major, minor)
569 headers = request.headers.copy()
570
571 # MIMENonMultipart adds its own Content-Type header.
572 if 'content-type' in headers:
573 del headers['content-type']
574
575 for key, value in headers.iteritems():
576 msg[key] = value
577 msg['Host'] = parsed.netloc
578 msg.set_unixfrom(None)
579
580 if request.body is not None:
581 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500582 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500583
584 body = msg.as_string(False)
585 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
586 if request.body is None:
587 body = body[:-2]
588
Joe Gregoriodd813822012-01-25 10:32:47 -0500589 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500590
591 def _deserialize_response(self, payload):
592 """Convert string into httplib2 response and content.
593
594 Args:
595 payload: string, headers and body as a string.
596
597 Returns:
598 A pair (resp, content) like would be returned from httplib2.request.
599 """
600 # Strip off the status line
601 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500602 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500603
604 # Parse the rest of the response
605 parser = FeedParser()
606 parser.feed(payload)
607 msg = parser.close()
608 msg['status'] = status
609
610 # Create httplib2.Response from the parsed headers.
611 resp = httplib2.Response(msg)
612 resp.reason = reason
613 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
614
615 content = payload.split('\r\n\r\n', 1)[1]
616
617 return resp, content
618
619 def _new_id(self):
620 """Create a new id.
621
622 Auto incrementing number that avoids conflicts with ids already used.
623
624 Returns:
625 string, a new unique id.
626 """
627 self._last_auto_id += 1
628 while str(self._last_auto_id) in self._requests:
629 self._last_auto_id += 1
630 return str(self._last_auto_id)
631
632 def add(self, request, callback=None, request_id=None):
633 """Add a new request.
634
635 Every callback added will be paired with a unique id, the request_id. That
636 unique id will be passed back to the callback when the response comes back
637 from the server. The default behavior is to have the library generate it's
638 own unique id. If the caller passes in a request_id then they must ensure
639 uniqueness for each request_id, and if they are not an exception is
640 raised. Callers should either supply all request_ids or nevery supply a
641 request id, to avoid such an error.
642
643 Args:
644 request: HttpRequest, Request to add to the batch.
645 callback: callable, A callback to be called for this response, of the
646 form callback(id, response). The first parameter is the request id, and
647 the second is the deserialized response object.
648 request_id: string, A unique id for the request. The id will be passed to
649 the callback with the response.
650
651 Returns:
652 None
653
654 Raises:
655 BatchError if a resumable request is added to a batch.
656 KeyError is the request_id is not unique.
657 """
658 if request_id is None:
659 request_id = self._new_id()
660 if request.resumable is not None:
661 raise BatchError("Resumable requests cannot be used in a batch request.")
662 if request_id in self._requests:
663 raise KeyError("A request with this ID already exists: %s" % request_id)
664 self._requests[request_id] = (request, callback)
665 self._order.append(request_id)
666
667 def execute(self, http=None):
668 """Execute all the requests as a single batched HTTP request.
669
670 Args:
671 http: httplib2.Http, an http object to be used in place of the one the
672 HttpRequest request object was constructed with. If one isn't supplied
673 then use a http object from the requests in this batch.
674
675 Returns:
676 None
677
678 Raises:
679 apiclient.errors.HttpError if the response was not a 2xx.
680 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500681 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -0500682 """
683 if http is None:
684 for request_id in self._order:
685 request, callback = self._requests[request_id]
686 if request is not None:
687 http = request.http
688 break
689 if http is None:
690 raise ValueError("Missing a valid http object.")
691
692
693 msgRoot = MIMEMultipart('mixed')
694 # msgRoot should not write out it's own headers
695 setattr(msgRoot, '_write_headers', lambda self: None)
696
697 # Add all the individual requests.
698 for request_id in self._order:
699 request, callback = self._requests[request_id]
700
701 msg = MIMENonMultipart('application', 'http')
702 msg['Content-Transfer-Encoding'] = 'binary'
703 msg['Content-ID'] = self._id_to_header(request_id)
704
705 body = self._serialize_request(request)
706 msg.set_payload(body)
707 msgRoot.attach(msg)
708
709 body = msgRoot.as_string()
710
711 headers = {}
712 headers['content-type'] = ('multipart/mixed; '
713 'boundary="%s"') % msgRoot.get_boundary()
714
715 resp, content = http.request(self._batch_uri, 'POST', body=body,
716 headers=headers)
717
718 if resp.status >= 300:
719 raise HttpError(resp, content, self._batch_uri)
720
721 # Now break up the response and process each one with the correct postproc
722 # and trigger the right callbacks.
723 boundary, _ = content.split(None, 1)
724
725 # Prepend with a content-type header so FeedParser can handle it.
Joe Gregoriodd813822012-01-25 10:32:47 -0500726 header = 'content-type: %s\r\n\r\n' % resp['content-type']
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500727 for_parser = header + content
Joe Gregorio66f57522011-11-30 11:00:00 -0500728
729 parser = FeedParser()
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500730 parser.feed(for_parser)
Joe Gregorio66f57522011-11-30 11:00:00 -0500731 respRoot = parser.close()
732
733 if not respRoot.is_multipart():
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500734 raise BatchError("Response not in multipart/mixed format.", resp,
735 content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500736
737 parts = respRoot.get_payload()
738 for part in parts:
739 request_id = self._header_to_id(part['Content-ID'])
740
741 headers, content = self._deserialize_response(part.get_payload())
742
743 # TODO(jcgregorio) Remove this temporary hack once the server stops
744 # gzipping individual response bodies.
745 if content[0] != '{':
746 gzipped_content = content
747 content = gzip.GzipFile(
748 fileobj=StringIO.StringIO(gzipped_content)).read()
749
750 request, cb = self._requests[request_id]
751 postproc = request.postproc
752 response = postproc(resp, content)
753 if cb is not None:
754 cb(request_id, response)
755 if self._callback is not None:
756 self._callback(request_id, response)
757
758
Joe Gregorioaf276d22010-12-09 14:26:58 -0500759class HttpRequestMock(object):
760 """Mock of HttpRequest.
761
762 Do not construct directly, instead use RequestMockBuilder.
763 """
764
765 def __init__(self, resp, content, postproc):
766 """Constructor for HttpRequestMock
767
768 Args:
769 resp: httplib2.Response, the response to emulate coming from the request
770 content: string, the response body
771 postproc: callable, the post processing function usually supplied by
772 the model class. See model.JsonModel.response() as an example.
773 """
774 self.resp = resp
775 self.content = content
776 self.postproc = postproc
777 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500778 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500779 if 'reason' in self.resp:
780 self.resp.reason = self.resp['reason']
781
782 def execute(self, http=None):
783 """Execute the request.
784
785 Same behavior as HttpRequest.execute(), but the response is
786 mocked and not really from an HTTP request/response.
787 """
788 return self.postproc(self.resp, self.content)
789
790
791class RequestMockBuilder(object):
792 """A simple mock of HttpRequest
793
794 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400795 tuples of (httplib2.Response, content, opt_expected_body) that should be
796 returned when that method is called. None may also be passed in for the
797 httplib2.Response, in which case a 200 OK response will be generated.
798 If an opt_expected_body (str or dict) is provided, it will be compared to
799 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500800
801 Example:
802 response = '{"data": {"id": "tag:google.c...'
803 requestBuilder = RequestMockBuilder(
804 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500805 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500806 }
807 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500808 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500809
810 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -0500811 200 OK with an empty string as the response content or raise an excpetion
812 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -0400813 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500814
815 For more details see the project wiki.
816 """
817
Joe Gregorioa388ce32011-09-09 17:19:13 -0400818 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500819 """Constructor for RequestMockBuilder
820
821 The constructed object should be a callable object
822 that can replace the class HttpResponse.
823
824 responses - A dictionary that maps methodIds into tuples
825 of (httplib2.Response, content). The methodId
826 comes from the 'rpcName' field in the discovery
827 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400828 check_unexpected - A boolean setting whether or not UnexpectedMethodError
829 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500830 """
831 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400832 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500833
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500834 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500835 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500836 """Implements the callable interface that discovery.build() expects
837 of requestBuilder, which is to build an object compatible with
838 HttpRequest.execute(). See that method for the description of the
839 parameters and the expected response.
840 """
841 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400842 response = self.responses[methodId]
843 resp, content = response[:2]
844 if len(response) > 2:
845 # Test the body against the supplied expected_body.
846 expected_body = response[2]
847 if bool(expected_body) != bool(body):
848 # Not expecting a body and provided one
849 # or expecting a body and not provided one.
850 raise UnexpectedBodyError(expected_body, body)
851 if isinstance(expected_body, str):
852 expected_body = simplejson.loads(expected_body)
853 body = simplejson.loads(body)
854 if body != expected_body:
855 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500856 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400857 elif self.check_unexpected:
858 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500859 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500860 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500861 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500862
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500863
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500864class HttpMock(object):
865 """Mock of httplib2.Http"""
866
Joe Gregorioec343652011-02-16 16:52:51 -0500867 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500868 """
869 Args:
870 filename: string, absolute filename to read response from
871 headers: dict, header to return with response
872 """
Joe Gregorioec343652011-02-16 16:52:51 -0500873 if headers is None:
874 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500875 f = file(filename, 'r')
876 self.data = f.read()
877 f.close()
878 self.headers = headers
879
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500880 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500881 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500882 body=None,
883 headers=None,
884 redirections=1,
885 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500886 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500887
888
889class HttpMockSequence(object):
890 """Mock of httplib2.Http
891
892 Mocks a sequence of calls to request returning different responses for each
893 call. Create an instance initialized with the desired response headers
894 and content and then use as if an httplib2.Http instance.
895
896 http = HttpMockSequence([
897 ({'status': '401'}, ''),
898 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
899 ({'status': '200'}, 'echo_request_headers'),
900 ])
901 resp, content = http.request("http://examples.com")
902
903 There are special values you can pass in for content to trigger
904 behavours that are helpful in testing.
905
906 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400907 'echo_request_headers_as_json' means return the request headers in
908 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500909 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400910 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500911 """
912
913 def __init__(self, iterable):
914 """
915 Args:
916 iterable: iterable, a sequence of pairs of (headers, body)
917 """
918 self._iterable = iterable
919
920 def request(self, uri,
921 method='GET',
922 body=None,
923 headers=None,
924 redirections=1,
925 connection_type=None):
926 resp, content = self._iterable.pop(0)
927 if content == 'echo_request_headers':
928 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400929 elif content == 'echo_request_headers_as_json':
930 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500931 elif content == 'echo_request_body':
932 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400933 elif content == 'echo_request_uri':
934 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -0500935 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500936
937
938def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400939 """Set the user-agent on every request.
940
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500941 Args:
942 http - An instance of httplib2.Http
943 or something that acts like it.
944 user_agent: string, the value for the user-agent header.
945
946 Returns:
947 A modified instance of http that was passed in.
948
949 Example:
950
951 h = httplib2.Http()
952 h = set_user_agent(h, "my-app-name/6.0")
953
954 Most of the time the user-agent will be set doing auth, this is for the rare
955 cases where you are accessing an unauthenticated endpoint.
956 """
957 request_orig = http.request
958
959 # The closure that will replace 'httplib2.Http.request'.
960 def new_request(uri, method='GET', body=None, headers=None,
961 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
962 connection_type=None):
963 """Modify the request headers to add the user-agent."""
964 if headers is None:
965 headers = {}
966 if 'user-agent' in headers:
967 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
968 else:
969 headers['user-agent'] = user_agent
970 resp, content = request_orig(uri, method, body, headers,
971 redirections, connection_type)
972 return resp, content
973
974 http.request = new_request
975 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400976
977
978def tunnel_patch(http):
979 """Tunnel PATCH requests over POST.
980 Args:
981 http - An instance of httplib2.Http
982 or something that acts like it.
983
984 Returns:
985 A modified instance of http that was passed in.
986
987 Example:
988
989 h = httplib2.Http()
990 h = tunnel_patch(h, "my-app-name/6.0")
991
992 Useful if you are running on a platform that doesn't support PATCH.
993 Apply this last if you are using OAuth 1.0, as changing the method
994 will result in a different signature.
995 """
996 request_orig = http.request
997
998 # The closure that will replace 'httplib2.Http.request'.
999 def new_request(uri, method='GET', body=None, headers=None,
1000 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1001 connection_type=None):
1002 """Modify the request headers to add the user-agent."""
1003 if headers is None:
1004 headers = {}
1005 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001006 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001007 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001008 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001009 headers['x-http-method-override'] = "PATCH"
1010 method = 'POST'
1011 resp, content = request_orig(uri, method, body, headers,
1012 redirections, connection_type)
1013 return resp, content
1014
1015 http.request = new_request
1016 return http