blob: 8e15e1116e5bbc922471f2a870347f4f88862489 [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
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import copy
Joe Gregorio66f57522011-11-30 11:00:00 -050030import gzip
Joe Gregorioc6722462010-12-20 14:29:28 -050031import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050032import mimeparse
33import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050034import os
35import urllib
36import urlparse
37import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050038
Joe Gregorio66f57522011-11-30 11:00:00 -050039from email.mime.multipart import MIMEMultipart
40from email.mime.nonmultipart import MIMENonMultipart
41from email.parser import FeedParser
42from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050043from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050044from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040045from errors import UnexpectedBodyError
46from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050047from model import JsonModel
Joe Gregorio549230c2012-01-11 10:38:05 -050048from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040049
50
Joe Gregoriod0bd3882011-11-22 09:49:47 -050051class MediaUploadProgress(object):
52 """Status of a resumable upload."""
53
54 def __init__(self, resumable_progress, total_size):
55 """Constructor.
56
57 Args:
58 resumable_progress: int, bytes sent so far.
59 total_size: int, total bytes in complete upload.
60 """
61 self.resumable_progress = resumable_progress
62 self.total_size = total_size
63
64 def progress(self):
65 """Percent of upload completed, as a float."""
Joe Gregorio66f57522011-11-30 11:00:00 -050066 return float(self.resumable_progress) / float(self.total_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -050067
68
69class MediaUpload(object):
70 """Describes a media object to upload.
71
72 Base class that defines the interface of MediaUpload subclasses.
73 """
74
75 def getbytes(self, begin, end):
76 raise NotImplementedError()
77
78 def size(self):
79 raise NotImplementedError()
80
81 def chunksize(self):
82 raise NotImplementedError()
83
84 def mimetype(self):
85 return 'application/octet-stream'
86
87 def resumable(self):
88 return False
89
90 def _to_json(self, strip=None):
91 """Utility function for creating a JSON representation of a MediaUpload.
92
93 Args:
94 strip: array, An array of names of members to not include in the JSON.
95
96 Returns:
97 string, a JSON representation of this instance, suitable to pass to
98 from_json().
99 """
100 t = type(self)
101 d = copy.copy(self.__dict__)
102 if strip is not None:
103 for member in strip:
104 del d[member]
105 d['_class'] = t.__name__
106 d['_module'] = t.__module__
107 return simplejson.dumps(d)
108
109 def to_json(self):
110 """Create a JSON representation of an instance of MediaUpload.
111
112 Returns:
113 string, a JSON representation of this instance, suitable to pass to
114 from_json().
115 """
116 return self._to_json()
117
118 @classmethod
119 def new_from_json(cls, s):
120 """Utility class method to instantiate a MediaUpload subclass from a JSON
121 representation produced by to_json().
122
123 Args:
124 s: string, JSON from to_json().
125
126 Returns:
127 An instance of the subclass of MediaUpload that was serialized with
128 to_json().
129 """
130 data = simplejson.loads(s)
131 # Find and call the right classmethod from_json() to restore the object.
132 module = data['_module']
133 m = __import__(module, fromlist=module.split('.')[:-1])
134 kls = getattr(m, data['_class'])
135 from_json = getattr(kls, 'from_json')
136 return from_json(s)
137
Joe Gregorio66f57522011-11-30 11:00:00 -0500138
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500139class MediaFileUpload(MediaUpload):
140 """A MediaUpload for a file.
141
142 Construct a MediaFileUpload and pass as the media_body parameter of the
143 method. For example, if we had a service that allowed uploading images:
144
145
146 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
147 resumable=True)
148 service.objects().insert(
149 bucket=buckets['items'][0]['id'],
150 name='smiley.png',
151 media_body=media).execute()
152 """
153
Joe Gregorio945be3e2012-01-27 17:01:06 -0500154 def __init__(self, filename, mimetype=None, chunksize=256*1024, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500155 """Constructor.
156
157 Args:
158 filename: string, Name of the file.
159 mimetype: string, Mime-type of the file. If None then a mime-type will be
160 guessed from the file extension.
161 chunksize: int, File will be uploaded in chunks of this many bytes. Only
162 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500163 resumable: bool, True if this is a resumable upload. False means upload
164 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500165 """
166 self._filename = filename
167 self._size = os.path.getsize(filename)
168 self._fd = None
169 if mimetype is None:
170 (mimetype, encoding) = mimetypes.guess_type(filename)
171 self._mimetype = mimetype
172 self._chunksize = chunksize
173 self._resumable = resumable
174
175 def mimetype(self):
176 return self._mimetype
177
178 def size(self):
179 return self._size
180
181 def chunksize(self):
182 return self._chunksize
183
184 def resumable(self):
185 return self._resumable
186
187 def getbytes(self, begin, length):
188 """Get bytes from the media.
189
190 Args:
191 begin: int, offset from beginning of file.
192 length: int, number of bytes to read, starting at begin.
193
194 Returns:
195 A string of bytes read. May be shorted than length if EOF was reached
196 first.
197 """
198 if self._fd is None:
199 self._fd = open(self._filename, 'rb')
200 self._fd.seek(begin)
201 return self._fd.read(length)
202
203 def to_json(self):
204 """Creating a JSON representation of an instance of Credentials.
205
206 Returns:
207 string, a JSON representation of this instance, suitable to pass to
208 from_json().
209 """
210 return self._to_json(['_fd'])
211
212 @staticmethod
213 def from_json(s):
214 d = simplejson.loads(s)
215 return MediaFileUpload(
216 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
217
218
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400219class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500220 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400221
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500222 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500223 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500224 body=None,
225 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500226 methodId=None,
227 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500228 """Constructor for an HttpRequest.
229
Joe Gregorioaf276d22010-12-09 14:26:58 -0500230 Args:
231 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500232 postproc: callable, called on the HTTP response and content to transform
233 it into a data object before returning, or raising an exception
234 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500235 uri: string, the absolute URI to send the request to
236 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500237 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500238 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500239 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500240 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500241 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400242 self.uri = uri
243 self.method = method
244 self.body = body
245 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500246 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400247 self.http = http
248 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500249 self.resumable = resumable
250
Joe Gregorio66f57522011-11-30 11:00:00 -0500251 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500252 major, minor, params = mimeparse.parse_mime_type(
253 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500254
Joe Gregorio945be3e2012-01-27 17:01:06 -0500255 # The size of the non-media part of the request.
256 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500257
258 # The resumable URI to send chunks to.
259 self.resumable_uri = None
260
261 # The bytes that have been uploaded.
262 self.resumable_progress = 0
263
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400264 def execute(self, http=None):
265 """Execute the request.
266
Joe Gregorioaf276d22010-12-09 14:26:58 -0500267 Args:
268 http: httplib2.Http, an http object to be used in place of the
269 one the HttpRequest request object was constructed with.
270
271 Returns:
272 A deserialized object model of the response body as determined
273 by the postproc.
274
275 Raises:
276 apiclient.errors.HttpError if the response was not a 2xx.
277 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400278 """
279 if http is None:
280 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500281 if self.resumable:
282 body = None
283 while body is None:
284 _, body = self.next_chunk(http)
285 return body
286 else:
287 resp, content = http.request(self.uri, self.method,
288 body=self.body,
289 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500290
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500291 if resp.status >= 300:
292 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400293 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500294
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500295 def next_chunk(self, http=None):
296 """Execute the next step of a resumable upload.
297
Joe Gregorio66f57522011-11-30 11:00:00 -0500298 Can only be used if the method being executed supports media uploads and
299 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500300
301 Example:
302
Joe Gregorio66f57522011-11-30 11:00:00 -0500303 media = MediaFileUpload('smiley.png', mimetype='image/png',
304 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500305 request = service.objects().insert(
306 bucket=buckets['items'][0]['id'],
307 name='smiley.png',
308 media_body=media)
309
310 response = None
311 while response is None:
312 status, response = request.next_chunk()
313 if status:
314 print "Upload %d%% complete." % int(status.progress() * 100)
315
316
317 Returns:
318 (status, body): (ResumableMediaStatus, object)
319 The body will be None until the resumable media is fully uploaded.
320 """
321 if http is None:
322 http = self.http
323
324 if self.resumable_uri is None:
325 start_headers = copy.copy(self.headers)
326 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
327 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
Joe Gregorio945be3e2012-01-27 17:01:06 -0500328 start_headers['content-length'] = str(self.body_size)
329
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500330 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500331 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500332 headers=start_headers)
333 if resp.status == 200 and 'location' in resp:
334 self.resumable_uri = resp['location']
335 else:
336 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500337
Joe Gregorio945be3e2012-01-27 17:01:06 -0500338 data = self.resumable.getbytes(self.resumable_progress,
339 self.resumable.chunksize())
340
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500341 headers = {
342 'Content-Range': 'bytes %d-%d/%d' % (
343 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500344 self.resumable.size()),
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500345 }
346 resp, content = http.request(self.resumable_uri, 'PUT',
347 body=data,
348 headers=headers)
349 if resp.status in [200, 201]:
350 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500351 elif resp.status == 308:
Joe Gregorio66f57522011-11-30 11:00:00 -0500352 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500353 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500354 if 'location' in resp:
355 self.resumable_uri = resp['location']
356 else:
357 raise HttpError(resp, content, self.uri)
358
Joe Gregorio945be3e2012-01-27 17:01:06 -0500359 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
360 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500361
362 def to_json(self):
363 """Returns a JSON representation of the HttpRequest."""
364 d = copy.copy(self.__dict__)
365 if d['resumable'] is not None:
366 d['resumable'] = self.resumable.to_json()
367 del d['http']
368 del d['postproc']
369 return simplejson.dumps(d)
370
371 @staticmethod
372 def from_json(s, http, postproc):
373 """Returns an HttpRequest populated with info from a JSON object."""
374 d = simplejson.loads(s)
375 if d['resumable'] is not None:
376 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
377 return HttpRequest(
378 http,
379 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500380 uri=d['uri'],
381 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500382 body=d['body'],
383 headers=d['headers'],
384 methodId=d['methodId'],
385 resumable=d['resumable'])
386
Joe Gregorioaf276d22010-12-09 14:26:58 -0500387
Joe Gregorio66f57522011-11-30 11:00:00 -0500388class BatchHttpRequest(object):
389 """Batches multiple HttpRequest objects into a single HTTP request."""
390
391 def __init__(self, callback=None, batch_uri=None):
392 """Constructor for a BatchHttpRequest.
393
394 Args:
395 callback: callable, A callback to be called for each response, of the
396 form callback(id, response). The first parameter is the request id, and
397 the second is the deserialized response object.
398 batch_uri: string, URI to send batch requests to.
399 """
400 if batch_uri is None:
401 batch_uri = 'https://www.googleapis.com/batch'
402 self._batch_uri = batch_uri
403
404 # Global callback to be called for each individual response in the batch.
405 self._callback = callback
406
407 # A map from id to (request, callback) pairs.
408 self._requests = {}
409
410 # List of request ids, in the order in which they were added.
411 self._order = []
412
413 # The last auto generated id.
414 self._last_auto_id = 0
415
416 # Unique ID on which to base the Content-ID headers.
417 self._base_id = None
418
419 def _id_to_header(self, id_):
420 """Convert an id to a Content-ID header value.
421
422 Args:
423 id_: string, identifier of individual request.
424
425 Returns:
426 A Content-ID header with the id_ encoded into it. A UUID is prepended to
427 the value because Content-ID headers are supposed to be universally
428 unique.
429 """
430 if self._base_id is None:
431 self._base_id = uuid.uuid4()
432
433 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
434
435 def _header_to_id(self, header):
436 """Convert a Content-ID header value to an id.
437
438 Presumes the Content-ID header conforms to the format that _id_to_header()
439 returns.
440
441 Args:
442 header: string, Content-ID header value.
443
444 Returns:
445 The extracted id value.
446
447 Raises:
448 BatchError if the header is not in the expected format.
449 """
450 if header[0] != '<' or header[-1] != '>':
451 raise BatchError("Invalid value for Content-ID: %s" % header)
452 if '+' not in header:
453 raise BatchError("Invalid value for Content-ID: %s" % header)
454 base, id_ = header[1:-1].rsplit('+', 1)
455
456 return urllib.unquote(id_)
457
458 def _serialize_request(self, request):
459 """Convert an HttpRequest object into a string.
460
461 Args:
462 request: HttpRequest, the request to serialize.
463
464 Returns:
465 The request as a string in application/http format.
466 """
467 # Construct status line
468 parsed = urlparse.urlparse(request.uri)
469 request_line = urlparse.urlunparse(
470 (None, None, parsed.path, parsed.params, parsed.query, None)
471 )
472 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500473 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500474 msg = MIMENonMultipart(major, minor)
475 headers = request.headers.copy()
476
477 # MIMENonMultipart adds its own Content-Type header.
478 if 'content-type' in headers:
479 del headers['content-type']
480
481 for key, value in headers.iteritems():
482 msg[key] = value
483 msg['Host'] = parsed.netloc
484 msg.set_unixfrom(None)
485
486 if request.body is not None:
487 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500488 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500489
490 body = msg.as_string(False)
491 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
492 if request.body is None:
493 body = body[:-2]
494
Joe Gregoriodd813822012-01-25 10:32:47 -0500495 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500496
497 def _deserialize_response(self, payload):
498 """Convert string into httplib2 response and content.
499
500 Args:
501 payload: string, headers and body as a string.
502
503 Returns:
504 A pair (resp, content) like would be returned from httplib2.request.
505 """
506 # Strip off the status line
507 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500508 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500509
510 # Parse the rest of the response
511 parser = FeedParser()
512 parser.feed(payload)
513 msg = parser.close()
514 msg['status'] = status
515
516 # Create httplib2.Response from the parsed headers.
517 resp = httplib2.Response(msg)
518 resp.reason = reason
519 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
520
521 content = payload.split('\r\n\r\n', 1)[1]
522
523 return resp, content
524
525 def _new_id(self):
526 """Create a new id.
527
528 Auto incrementing number that avoids conflicts with ids already used.
529
530 Returns:
531 string, a new unique id.
532 """
533 self._last_auto_id += 1
534 while str(self._last_auto_id) in self._requests:
535 self._last_auto_id += 1
536 return str(self._last_auto_id)
537
538 def add(self, request, callback=None, request_id=None):
539 """Add a new request.
540
541 Every callback added will be paired with a unique id, the request_id. That
542 unique id will be passed back to the callback when the response comes back
543 from the server. The default behavior is to have the library generate it's
544 own unique id. If the caller passes in a request_id then they must ensure
545 uniqueness for each request_id, and if they are not an exception is
546 raised. Callers should either supply all request_ids or nevery supply a
547 request id, to avoid such an error.
548
549 Args:
550 request: HttpRequest, Request to add to the batch.
551 callback: callable, A callback to be called for this response, of the
552 form callback(id, response). The first parameter is the request id, and
553 the second is the deserialized response object.
554 request_id: string, A unique id for the request. The id will be passed to
555 the callback with the response.
556
557 Returns:
558 None
559
560 Raises:
561 BatchError if a resumable request is added to a batch.
562 KeyError is the request_id is not unique.
563 """
564 if request_id is None:
565 request_id = self._new_id()
566 if request.resumable is not None:
567 raise BatchError("Resumable requests cannot be used in a batch request.")
568 if request_id in self._requests:
569 raise KeyError("A request with this ID already exists: %s" % request_id)
570 self._requests[request_id] = (request, callback)
571 self._order.append(request_id)
572
573 def execute(self, http=None):
574 """Execute all the requests as a single batched HTTP request.
575
576 Args:
577 http: httplib2.Http, an http object to be used in place of the one the
578 HttpRequest request object was constructed with. If one isn't supplied
579 then use a http object from the requests in this batch.
580
581 Returns:
582 None
583
584 Raises:
585 apiclient.errors.HttpError if the response was not a 2xx.
586 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500587 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -0500588 """
589 if http is None:
590 for request_id in self._order:
591 request, callback = self._requests[request_id]
592 if request is not None:
593 http = request.http
594 break
595 if http is None:
596 raise ValueError("Missing a valid http object.")
597
598
599 msgRoot = MIMEMultipart('mixed')
600 # msgRoot should not write out it's own headers
601 setattr(msgRoot, '_write_headers', lambda self: None)
602
603 # Add all the individual requests.
604 for request_id in self._order:
605 request, callback = self._requests[request_id]
606
607 msg = MIMENonMultipart('application', 'http')
608 msg['Content-Transfer-Encoding'] = 'binary'
609 msg['Content-ID'] = self._id_to_header(request_id)
610
611 body = self._serialize_request(request)
612 msg.set_payload(body)
613 msgRoot.attach(msg)
614
615 body = msgRoot.as_string()
616
617 headers = {}
618 headers['content-type'] = ('multipart/mixed; '
619 'boundary="%s"') % msgRoot.get_boundary()
620
621 resp, content = http.request(self._batch_uri, 'POST', body=body,
622 headers=headers)
623
624 if resp.status >= 300:
625 raise HttpError(resp, content, self._batch_uri)
626
627 # Now break up the response and process each one with the correct postproc
628 # and trigger the right callbacks.
629 boundary, _ = content.split(None, 1)
630
631 # Prepend with a content-type header so FeedParser can handle it.
Joe Gregoriodd813822012-01-25 10:32:47 -0500632 header = 'content-type: %s\r\n\r\n' % resp['content-type']
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500633 for_parser = header + content
Joe Gregorio66f57522011-11-30 11:00:00 -0500634
635 parser = FeedParser()
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500636 parser.feed(for_parser)
Joe Gregorio66f57522011-11-30 11:00:00 -0500637 respRoot = parser.close()
638
639 if not respRoot.is_multipart():
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500640 raise BatchError("Response not in multipart/mixed format.", resp,
641 content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500642
643 parts = respRoot.get_payload()
644 for part in parts:
645 request_id = self._header_to_id(part['Content-ID'])
646
647 headers, content = self._deserialize_response(part.get_payload())
648
649 # TODO(jcgregorio) Remove this temporary hack once the server stops
650 # gzipping individual response bodies.
651 if content[0] != '{':
652 gzipped_content = content
653 content = gzip.GzipFile(
654 fileobj=StringIO.StringIO(gzipped_content)).read()
655
656 request, cb = self._requests[request_id]
657 postproc = request.postproc
658 response = postproc(resp, content)
659 if cb is not None:
660 cb(request_id, response)
661 if self._callback is not None:
662 self._callback(request_id, response)
663
664
Joe Gregorioaf276d22010-12-09 14:26:58 -0500665class HttpRequestMock(object):
666 """Mock of HttpRequest.
667
668 Do not construct directly, instead use RequestMockBuilder.
669 """
670
671 def __init__(self, resp, content, postproc):
672 """Constructor for HttpRequestMock
673
674 Args:
675 resp: httplib2.Response, the response to emulate coming from the request
676 content: string, the response body
677 postproc: callable, the post processing function usually supplied by
678 the model class. See model.JsonModel.response() as an example.
679 """
680 self.resp = resp
681 self.content = content
682 self.postproc = postproc
683 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500684 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500685 if 'reason' in self.resp:
686 self.resp.reason = self.resp['reason']
687
688 def execute(self, http=None):
689 """Execute the request.
690
691 Same behavior as HttpRequest.execute(), but the response is
692 mocked and not really from an HTTP request/response.
693 """
694 return self.postproc(self.resp, self.content)
695
696
697class RequestMockBuilder(object):
698 """A simple mock of HttpRequest
699
700 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400701 tuples of (httplib2.Response, content, opt_expected_body) that should be
702 returned when that method is called. None may also be passed in for the
703 httplib2.Response, in which case a 200 OK response will be generated.
704 If an opt_expected_body (str or dict) is provided, it will be compared to
705 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500706
707 Example:
708 response = '{"data": {"id": "tag:google.c...'
709 requestBuilder = RequestMockBuilder(
710 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500711 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500712 }
713 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500714 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500715
716 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -0500717 200 OK with an empty string as the response content or raise an excpetion
718 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -0400719 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500720
721 For more details see the project wiki.
722 """
723
Joe Gregorioa388ce32011-09-09 17:19:13 -0400724 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500725 """Constructor for RequestMockBuilder
726
727 The constructed object should be a callable object
728 that can replace the class HttpResponse.
729
730 responses - A dictionary that maps methodIds into tuples
731 of (httplib2.Response, content). The methodId
732 comes from the 'rpcName' field in the discovery
733 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400734 check_unexpected - A boolean setting whether or not UnexpectedMethodError
735 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500736 """
737 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400738 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500739
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500740 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500741 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500742 """Implements the callable interface that discovery.build() expects
743 of requestBuilder, which is to build an object compatible with
744 HttpRequest.execute(). See that method for the description of the
745 parameters and the expected response.
746 """
747 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400748 response = self.responses[methodId]
749 resp, content = response[:2]
750 if len(response) > 2:
751 # Test the body against the supplied expected_body.
752 expected_body = response[2]
753 if bool(expected_body) != bool(body):
754 # Not expecting a body and provided one
755 # or expecting a body and not provided one.
756 raise UnexpectedBodyError(expected_body, body)
757 if isinstance(expected_body, str):
758 expected_body = simplejson.loads(expected_body)
759 body = simplejson.loads(body)
760 if body != expected_body:
761 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500762 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400763 elif self.check_unexpected:
764 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500765 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500766 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500767 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500768
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500769
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500770class HttpMock(object):
771 """Mock of httplib2.Http"""
772
Joe Gregorioec343652011-02-16 16:52:51 -0500773 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500774 """
775 Args:
776 filename: string, absolute filename to read response from
777 headers: dict, header to return with response
778 """
Joe Gregorioec343652011-02-16 16:52:51 -0500779 if headers is None:
780 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500781 f = file(filename, 'r')
782 self.data = f.read()
783 f.close()
784 self.headers = headers
785
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500786 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500787 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500788 body=None,
789 headers=None,
790 redirections=1,
791 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500792 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500793
794
795class HttpMockSequence(object):
796 """Mock of httplib2.Http
797
798 Mocks a sequence of calls to request returning different responses for each
799 call. Create an instance initialized with the desired response headers
800 and content and then use as if an httplib2.Http instance.
801
802 http = HttpMockSequence([
803 ({'status': '401'}, ''),
804 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
805 ({'status': '200'}, 'echo_request_headers'),
806 ])
807 resp, content = http.request("http://examples.com")
808
809 There are special values you can pass in for content to trigger
810 behavours that are helpful in testing.
811
812 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400813 'echo_request_headers_as_json' means return the request headers in
814 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500815 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400816 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500817 """
818
819 def __init__(self, iterable):
820 """
821 Args:
822 iterable: iterable, a sequence of pairs of (headers, body)
823 """
824 self._iterable = iterable
825
826 def request(self, uri,
827 method='GET',
828 body=None,
829 headers=None,
830 redirections=1,
831 connection_type=None):
832 resp, content = self._iterable.pop(0)
833 if content == 'echo_request_headers':
834 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -0400835 elif content == 'echo_request_headers_as_json':
836 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -0500837 elif content == 'echo_request_body':
838 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400839 elif content == 'echo_request_uri':
840 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -0500841 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500842
843
844def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -0400845 """Set the user-agent on every request.
846
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500847 Args:
848 http - An instance of httplib2.Http
849 or something that acts like it.
850 user_agent: string, the value for the user-agent header.
851
852 Returns:
853 A modified instance of http that was passed in.
854
855 Example:
856
857 h = httplib2.Http()
858 h = set_user_agent(h, "my-app-name/6.0")
859
860 Most of the time the user-agent will be set doing auth, this is for the rare
861 cases where you are accessing an unauthenticated endpoint.
862 """
863 request_orig = http.request
864
865 # The closure that will replace 'httplib2.Http.request'.
866 def new_request(uri, method='GET', body=None, headers=None,
867 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
868 connection_type=None):
869 """Modify the request headers to add the user-agent."""
870 if headers is None:
871 headers = {}
872 if 'user-agent' in headers:
873 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
874 else:
875 headers['user-agent'] = user_agent
876 resp, content = request_orig(uri, method, body, headers,
877 redirections, connection_type)
878 return resp, content
879
880 http.request = new_request
881 return http
Joe Gregoriof4153422011-03-18 22:45:18 -0400882
883
884def tunnel_patch(http):
885 """Tunnel PATCH requests over POST.
886 Args:
887 http - An instance of httplib2.Http
888 or something that acts like it.
889
890 Returns:
891 A modified instance of http that was passed in.
892
893 Example:
894
895 h = httplib2.Http()
896 h = tunnel_patch(h, "my-app-name/6.0")
897
898 Useful if you are running on a platform that doesn't support PATCH.
899 Apply this last if you are using OAuth 1.0, as changing the method
900 will result in a different signature.
901 """
902 request_orig = http.request
903
904 # The closure that will replace 'httplib2.Http.request'.
905 def new_request(uri, method='GET', body=None, headers=None,
906 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
907 connection_type=None):
908 """Modify the request headers to add the user-agent."""
909 if headers is None:
910 headers = {}
911 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -0400912 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400913 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -0400914 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -0400915 headers['x-http-method-override'] = "PATCH"
916 method = 'POST'
917 resp, content = request_orig(uri, method, body, headers,
918 redirections, connection_type)
919 return resp, content
920
921 http.request = new_request
922 return http