blob: ff61cb1bfa6ce9b04be6da88c4c71462e9de365f [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 Gregorio654f4a22012-02-09 14:15:44 -050040from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050041from email.mime.multipart import MIMEMultipart
42from email.mime.nonmultipart import MIMENonMultipart
43from email.parser import FeedParser
44from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050045from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050046from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040047from errors import UnexpectedBodyError
48from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050049from model import JsonModel
Joe Gregorio549230c2012-01-11 10:38:05 -050050from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040051
52
Joe Gregoriod0bd3882011-11-22 09:49:47 -050053class MediaUploadProgress(object):
54 """Status of a resumable upload."""
55
56 def __init__(self, resumable_progress, total_size):
57 """Constructor.
58
59 Args:
60 resumable_progress: int, bytes sent so far.
61 total_size: int, total bytes in complete upload.
62 """
63 self.resumable_progress = resumable_progress
64 self.total_size = total_size
65
66 def progress(self):
67 """Percent of upload completed, as a float."""
Joe Gregorio66f57522011-11-30 11:00:00 -050068 return float(self.resumable_progress) / float(self.total_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -050069
70
71class MediaUpload(object):
72 """Describes a media object to upload.
73
74 Base class that defines the interface of MediaUpload subclasses.
75 """
76
77 def getbytes(self, begin, end):
78 raise NotImplementedError()
79
80 def size(self):
81 raise NotImplementedError()
82
83 def chunksize(self):
84 raise NotImplementedError()
85
86 def mimetype(self):
87 return 'application/octet-stream'
88
89 def resumable(self):
90 return False
91
92 def _to_json(self, strip=None):
93 """Utility function for creating a JSON representation of a MediaUpload.
94
95 Args:
96 strip: array, An array of names of members to not include in the JSON.
97
98 Returns:
99 string, a JSON representation of this instance, suitable to pass to
100 from_json().
101 """
102 t = type(self)
103 d = copy.copy(self.__dict__)
104 if strip is not None:
105 for member in strip:
106 del d[member]
107 d['_class'] = t.__name__
108 d['_module'] = t.__module__
109 return simplejson.dumps(d)
110
111 def to_json(self):
112 """Create a JSON representation of an instance of MediaUpload.
113
114 Returns:
115 string, a JSON representation of this instance, suitable to pass to
116 from_json().
117 """
118 return self._to_json()
119
120 @classmethod
121 def new_from_json(cls, s):
122 """Utility class method to instantiate a MediaUpload subclass from a JSON
123 representation produced by to_json().
124
125 Args:
126 s: string, JSON from to_json().
127
128 Returns:
129 An instance of the subclass of MediaUpload that was serialized with
130 to_json().
131 """
132 data = simplejson.loads(s)
133 # Find and call the right classmethod from_json() to restore the object.
134 module = data['_module']
135 m = __import__(module, fromlist=module.split('.')[:-1])
136 kls = getattr(m, data['_class'])
137 from_json = getattr(kls, 'from_json')
138 return from_json(s)
139
Joe Gregorio66f57522011-11-30 11:00:00 -0500140
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500141class MediaFileUpload(MediaUpload):
142 """A MediaUpload for a file.
143
144 Construct a MediaFileUpload and pass as the media_body parameter of the
145 method. For example, if we had a service that allowed uploading images:
146
147
148 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
149 resumable=True)
150 service.objects().insert(
151 bucket=buckets['items'][0]['id'],
152 name='smiley.png',
153 media_body=media).execute()
154 """
155
Joe Gregorio945be3e2012-01-27 17:01:06 -0500156 def __init__(self, filename, mimetype=None, chunksize=256*1024, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500157 """Constructor.
158
159 Args:
160 filename: string, Name of the file.
161 mimetype: string, Mime-type of the file. If None then a mime-type will be
162 guessed from the file extension.
163 chunksize: int, File will be uploaded in chunks of this many bytes. Only
164 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500165 resumable: bool, True if this is a resumable upload. False means upload
166 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500167 """
168 self._filename = filename
169 self._size = os.path.getsize(filename)
170 self._fd = None
171 if mimetype is None:
172 (mimetype, encoding) = mimetypes.guess_type(filename)
173 self._mimetype = mimetype
174 self._chunksize = chunksize
175 self._resumable = resumable
176
177 def mimetype(self):
178 return self._mimetype
179
180 def size(self):
181 return self._size
182
183 def chunksize(self):
184 return self._chunksize
185
186 def resumable(self):
187 return self._resumable
188
189 def getbytes(self, begin, length):
190 """Get bytes from the media.
191
192 Args:
193 begin: int, offset from beginning of file.
194 length: int, number of bytes to read, starting at begin.
195
196 Returns:
197 A string of bytes read. May be shorted than length if EOF was reached
198 first.
199 """
200 if self._fd is None:
201 self._fd = open(self._filename, 'rb')
202 self._fd.seek(begin)
203 return self._fd.read(length)
204
205 def to_json(self):
206 """Creating a JSON representation of an instance of Credentials.
207
208 Returns:
209 string, a JSON representation of this instance, suitable to pass to
210 from_json().
211 """
212 return self._to_json(['_fd'])
213
214 @staticmethod
215 def from_json(s):
216 d = simplejson.loads(s)
217 return MediaFileUpload(
218 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
219
220
Ali Afshar6f11ea12012-02-07 10:32:14 -0500221class MediaInMemoryUpload(MediaUpload):
222 """MediaUpload for a chunk of bytes.
223
224 Construct a MediaFileUpload and pass as the media_body parameter of the
225 method. For example, if we had a service that allowed plain text:
226 """
227
228 def __init__(self, body, mimetype='application/octet-stream',
229 chunksize=256*1024, resumable=False):
230 """Create a new MediaBytesUpload.
231
232 Args:
233 body: string, Bytes of body content.
234 mimetype: string, Mime-type of the file or default of
235 'application/octet-stream'.
236 chunksize: int, File will be uploaded in chunks of this many bytes. Only
237 used if resumable=True.
238 resumable: bool, True if this is a resumable upload. False means upload
239 in a single request.
240 """
241 self._body = body
242 self._mimetype = mimetype
243 self._resumable = resumable
244 self._chunksize = chunksize
245
246 def chunksize(self):
247 """Chunk size for resumable uploads.
248
249 Returns:
250 Chunk size in bytes.
251 """
252 return self._chunksize
253
254 def mimetype(self):
255 """Mime type of the body.
256
257 Returns:
258 Mime type.
259 """
260 return self._mimetype
261
262 def size(self):
263 """Size of upload.
264
265 Returns:
266 Size of the body.
267 """
268 return len(self.body)
269
270 def resumable(self):
271 """Whether this upload is resumable.
272
273 Returns:
274 True if resumable upload or False.
275 """
276 return self._resumable
277
278 def getbytes(self, begin, length):
279 """Get bytes from the media.
280
281 Args:
282 begin: int, offset from beginning of file.
283 length: int, number of bytes to read, starting at begin.
284
285 Returns:
286 A string of bytes read. May be shorter than length if EOF was reached
287 first.
288 """
289 return self._body[begin:begin + length]
290
291 def to_json(self):
292 """Create a JSON representation of a MediaInMemoryUpload.
293
294 Returns:
295 string, a JSON representation of this instance, suitable to pass to
296 from_json().
297 """
298 t = type(self)
299 d = copy.copy(self.__dict__)
300 del d['_body']
301 d['_class'] = t.__name__
302 d['_module'] = t.__module__
303 d['_b64body'] = base64.b64encode(self._body)
304 return simplejson.dumps(d)
305
306 @staticmethod
307 def from_json(s):
308 d = simplejson.loads(s)
309 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
310 d['_mimetype'], d['_chunksize'],
311 d['_resumable'])
312
313
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400314class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500315 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400316
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500317 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500318 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500319 body=None,
320 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500321 methodId=None,
322 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500323 """Constructor for an HttpRequest.
324
Joe Gregorioaf276d22010-12-09 14:26:58 -0500325 Args:
326 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500327 postproc: callable, called on the HTTP response and content to transform
328 it into a data object before returning, or raising an exception
329 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500330 uri: string, the absolute URI to send the request to
331 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500332 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500333 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500334 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500335 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500336 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400337 self.uri = uri
338 self.method = method
339 self.body = body
340 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500341 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400342 self.http = http
343 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500344 self.resumable = resumable
345
Joe Gregorio66f57522011-11-30 11:00:00 -0500346 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500347 major, minor, params = mimeparse.parse_mime_type(
348 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500349
Joe Gregorio945be3e2012-01-27 17:01:06 -0500350 # The size of the non-media part of the request.
351 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500352
353 # The resumable URI to send chunks to.
354 self.resumable_uri = None
355
356 # The bytes that have been uploaded.
357 self.resumable_progress = 0
358
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400359 def execute(self, http=None):
360 """Execute the request.
361
Joe Gregorioaf276d22010-12-09 14:26:58 -0500362 Args:
363 http: httplib2.Http, an http object to be used in place of the
364 one the HttpRequest request object was constructed with.
365
366 Returns:
367 A deserialized object model of the response body as determined
368 by the postproc.
369
370 Raises:
371 apiclient.errors.HttpError if the response was not a 2xx.
372 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400373 """
374 if http is None:
375 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500376 if self.resumable:
377 body = None
378 while body is None:
379 _, body = self.next_chunk(http)
380 return body
381 else:
382 resp, content = http.request(self.uri, self.method,
383 body=self.body,
384 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500385
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500386 if resp.status >= 300:
387 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400388 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500389
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500390 def next_chunk(self, http=None):
391 """Execute the next step of a resumable upload.
392
Joe Gregorio66f57522011-11-30 11:00:00 -0500393 Can only be used if the method being executed supports media uploads and
394 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500395
396 Example:
397
Joe Gregorio66f57522011-11-30 11:00:00 -0500398 media = MediaFileUpload('smiley.png', mimetype='image/png',
399 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500400 request = service.objects().insert(
401 bucket=buckets['items'][0]['id'],
402 name='smiley.png',
403 media_body=media)
404
405 response = None
406 while response is None:
407 status, response = request.next_chunk()
408 if status:
409 print "Upload %d%% complete." % int(status.progress() * 100)
410
411
412 Returns:
413 (status, body): (ResumableMediaStatus, object)
414 The body will be None until the resumable media is fully uploaded.
415 """
416 if http is None:
417 http = self.http
418
419 if self.resumable_uri is None:
420 start_headers = copy.copy(self.headers)
421 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
422 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
Joe Gregorio945be3e2012-01-27 17:01:06 -0500423 start_headers['content-length'] = str(self.body_size)
424
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500425 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500426 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500427 headers=start_headers)
428 if resp.status == 200 and 'location' in resp:
429 self.resumable_uri = resp['location']
430 else:
431 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500432
Joe Gregorio945be3e2012-01-27 17:01:06 -0500433 data = self.resumable.getbytes(self.resumable_progress,
434 self.resumable.chunksize())
435
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500436 headers = {
437 'Content-Range': 'bytes %d-%d/%d' % (
438 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500439 self.resumable.size()),
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500440 }
441 resp, content = http.request(self.resumable_uri, 'PUT',
442 body=data,
443 headers=headers)
444 if resp.status in [200, 201]:
445 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500446 elif resp.status == 308:
Joe Gregorio66f57522011-11-30 11:00:00 -0500447 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500448 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500449 if 'location' in resp:
450 self.resumable_uri = resp['location']
451 else:
452 raise HttpError(resp, content, self.uri)
453
Joe Gregorio945be3e2012-01-27 17:01:06 -0500454 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
455 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500456
457 def to_json(self):
458 """Returns a JSON representation of the HttpRequest."""
459 d = copy.copy(self.__dict__)
460 if d['resumable'] is not None:
461 d['resumable'] = self.resumable.to_json()
462 del d['http']
463 del d['postproc']
464 return simplejson.dumps(d)
465
466 @staticmethod
467 def from_json(s, http, postproc):
468 """Returns an HttpRequest populated with info from a JSON object."""
469 d = simplejson.loads(s)
470 if d['resumable'] is not None:
471 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
472 return HttpRequest(
473 http,
474 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500475 uri=d['uri'],
476 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500477 body=d['body'],
478 headers=d['headers'],
479 methodId=d['methodId'],
480 resumable=d['resumable'])
481
Joe Gregorioaf276d22010-12-09 14:26:58 -0500482
Joe Gregorio66f57522011-11-30 11:00:00 -0500483class BatchHttpRequest(object):
484 """Batches multiple HttpRequest objects into a single HTTP request."""
485
486 def __init__(self, callback=None, batch_uri=None):
487 """Constructor for a BatchHttpRequest.
488
489 Args:
490 callback: callable, A callback to be called for each response, of the
491 form callback(id, response). The first parameter is the request id, and
492 the second is the deserialized response object.
493 batch_uri: string, URI to send batch requests to.
494 """
495 if batch_uri is None:
496 batch_uri = 'https://www.googleapis.com/batch'
497 self._batch_uri = batch_uri
498
499 # Global callback to be called for each individual response in the batch.
500 self._callback = callback
501
Joe Gregorio654f4a22012-02-09 14:15:44 -0500502 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500503 self._requests = {}
504
Joe Gregorio654f4a22012-02-09 14:15:44 -0500505 # A map from id to callback.
506 self._callbacks = {}
507
Joe Gregorio66f57522011-11-30 11:00:00 -0500508 # List of request ids, in the order in which they were added.
509 self._order = []
510
511 # The last auto generated id.
512 self._last_auto_id = 0
513
514 # Unique ID on which to base the Content-ID headers.
515 self._base_id = None
516
Joe Gregorio654f4a22012-02-09 14:15:44 -0500517 # A map from request id to (headers, content) response pairs
518 self._responses = {}
519
520 # A map of id(Credentials) that have been refreshed.
521 self._refreshed_credentials = {}
522
523 def _refresh_and_apply_credentials(self, request, http):
524 """Refresh the credentials and apply to the request.
525
526 Args:
527 request: HttpRequest, the request.
528 http: httplib2.Http, the global http object for the batch.
529 """
530 # For the credentials to refresh, but only once per refresh_token
531 # If there is no http per the request then refresh the http passed in
532 # via execute()
533 creds = None
534 if request.http is not None and hasattr(request.http.request,
535 'credentials'):
536 creds = request.http.request.credentials
537 elif http is not None and hasattr(http.request, 'credentials'):
538 creds = http.request.credentials
539 if creds is not None:
540 if id(creds) not in self._refreshed_credentials:
541 creds.refresh(http)
542 self._refreshed_credentials[id(creds)] = 1
543
544 # Only apply the credentials if we are using the http object passed in,
545 # otherwise apply() will get called during _serialize_request().
546 if request.http is None or not hasattr(request.http.request,
547 'credentials'):
548 creds.apply(request.headers)
549
Joe Gregorio66f57522011-11-30 11:00:00 -0500550 def _id_to_header(self, id_):
551 """Convert an id to a Content-ID header value.
552
553 Args:
554 id_: string, identifier of individual request.
555
556 Returns:
557 A Content-ID header with the id_ encoded into it. A UUID is prepended to
558 the value because Content-ID headers are supposed to be universally
559 unique.
560 """
561 if self._base_id is None:
562 self._base_id = uuid.uuid4()
563
564 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
565
566 def _header_to_id(self, header):
567 """Convert a Content-ID header value to an id.
568
569 Presumes the Content-ID header conforms to the format that _id_to_header()
570 returns.
571
572 Args:
573 header: string, Content-ID header value.
574
575 Returns:
576 The extracted id value.
577
578 Raises:
579 BatchError if the header is not in the expected format.
580 """
581 if header[0] != '<' or header[-1] != '>':
582 raise BatchError("Invalid value for Content-ID: %s" % header)
583 if '+' not in header:
584 raise BatchError("Invalid value for Content-ID: %s" % header)
585 base, id_ = header[1:-1].rsplit('+', 1)
586
587 return urllib.unquote(id_)
588
589 def _serialize_request(self, request):
590 """Convert an HttpRequest object into a string.
591
592 Args:
593 request: HttpRequest, the request to serialize.
594
595 Returns:
596 The request as a string in application/http format.
597 """
598 # Construct status line
599 parsed = urlparse.urlparse(request.uri)
600 request_line = urlparse.urlunparse(
601 (None, None, parsed.path, parsed.params, parsed.query, None)
602 )
603 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500604 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500605 msg = MIMENonMultipart(major, minor)
606 headers = request.headers.copy()
607
Joe Gregorio654f4a22012-02-09 14:15:44 -0500608 if request.http is not None and hasattr(request.http.request,
609 'credentials'):
610 request.http.request.credentials.apply(headers)
611
Joe Gregorio66f57522011-11-30 11:00:00 -0500612 # MIMENonMultipart adds its own Content-Type header.
613 if 'content-type' in headers:
614 del headers['content-type']
615
616 for key, value in headers.iteritems():
617 msg[key] = value
618 msg['Host'] = parsed.netloc
619 msg.set_unixfrom(None)
620
621 if request.body is not None:
622 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500623 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500624
Joe Gregorio654f4a22012-02-09 14:15:44 -0500625 # Serialize the mime message.
626 fp = StringIO.StringIO()
627 # maxheaderlen=0 means don't line wrap headers.
628 g = Generator(fp, maxheaderlen=0)
629 g.flatten(msg, unixfrom=False)
630 body = fp.getvalue()
631
Joe Gregorio66f57522011-11-30 11:00:00 -0500632 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
633 if request.body is None:
634 body = body[:-2]
635
Joe Gregoriodd813822012-01-25 10:32:47 -0500636 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500637
638 def _deserialize_response(self, payload):
639 """Convert string into httplib2 response and content.
640
641 Args:
642 payload: string, headers and body as a string.
643
644 Returns:
645 A pair (resp, content) like would be returned from httplib2.request.
646 """
647 # Strip off the status line
648 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500649 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500650
651 # Parse the rest of the response
652 parser = FeedParser()
653 parser.feed(payload)
654 msg = parser.close()
655 msg['status'] = status
656
657 # Create httplib2.Response from the parsed headers.
658 resp = httplib2.Response(msg)
659 resp.reason = reason
660 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
661
662 content = payload.split('\r\n\r\n', 1)[1]
663
664 return resp, content
665
666 def _new_id(self):
667 """Create a new id.
668
669 Auto incrementing number that avoids conflicts with ids already used.
670
671 Returns:
672 string, a new unique id.
673 """
674 self._last_auto_id += 1
675 while str(self._last_auto_id) in self._requests:
676 self._last_auto_id += 1
677 return str(self._last_auto_id)
678
679 def add(self, request, callback=None, request_id=None):
680 """Add a new request.
681
682 Every callback added will be paired with a unique id, the request_id. That
683 unique id will be passed back to the callback when the response comes back
684 from the server. The default behavior is to have the library generate it's
685 own unique id. If the caller passes in a request_id then they must ensure
686 uniqueness for each request_id, and if they are not an exception is
687 raised. Callers should either supply all request_ids or nevery supply a
688 request id, to avoid such an error.
689
690 Args:
691 request: HttpRequest, Request to add to the batch.
692 callback: callable, A callback to be called for this response, of the
693 form callback(id, response). The first parameter is the request id, and
694 the second is the deserialized response object.
695 request_id: string, A unique id for the request. The id will be passed to
696 the callback with the response.
697
698 Returns:
699 None
700
701 Raises:
702 BatchError if a resumable request is added to a batch.
703 KeyError is the request_id is not unique.
704 """
705 if request_id is None:
706 request_id = self._new_id()
707 if request.resumable is not None:
708 raise BatchError("Resumable requests cannot be used in a batch request.")
709 if request_id in self._requests:
710 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500711 self._requests[request_id] = request
712 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -0500713 self._order.append(request_id)
714
Joe Gregorio654f4a22012-02-09 14:15:44 -0500715 def _execute(self, http, order, requests):
716 """Serialize batch request, send to server, process response.
717
718 Args:
719 http: httplib2.Http, an http object to be used to make the request with.
720 order: list, list of request ids in the order they were added to the
721 batch.
722 request: list, list of request objects to send.
723
724 Raises:
725 httplib2.Error if a transport error has occured.
726 apiclient.errors.BatchError if the response is the wrong format.
727 """
728 message = MIMEMultipart('mixed')
729 # Message should not write out it's own headers.
730 setattr(message, '_write_headers', lambda self: None)
731
732 # Add all the individual requests.
733 for request_id in order:
734 request = requests[request_id]
735
736 msg = MIMENonMultipart('application', 'http')
737 msg['Content-Transfer-Encoding'] = 'binary'
738 msg['Content-ID'] = self._id_to_header(request_id)
739
740 body = self._serialize_request(request)
741 msg.set_payload(body)
742 message.attach(msg)
743
744 body = message.as_string()
745
746 headers = {}
747 headers['content-type'] = ('multipart/mixed; '
748 'boundary="%s"') % message.get_boundary()
749
750 resp, content = http.request(self._batch_uri, 'POST', body=body,
751 headers=headers)
752
753 if resp.status >= 300:
754 raise HttpError(resp, content, self._batch_uri)
755
756 # Now break out the individual responses and store each one.
757 boundary, _ = content.split(None, 1)
758
759 # Prepend with a content-type header so FeedParser can handle it.
760 header = 'content-type: %s\r\n\r\n' % resp['content-type']
761 for_parser = header + content
762
763 parser = FeedParser()
764 parser.feed(for_parser)
765 mime_response = parser.close()
766
767 if not mime_response.is_multipart():
768 raise BatchError("Response not in multipart/mixed format.", resp,
769 content)
770
771 for part in mime_response.get_payload():
772 request_id = self._header_to_id(part['Content-ID'])
773 headers, content = self._deserialize_response(part.get_payload())
774 self._responses[request_id] = (headers, content)
775
Joe Gregorio66f57522011-11-30 11:00:00 -0500776 def execute(self, http=None):
777 """Execute all the requests as a single batched HTTP request.
778
779 Args:
780 http: httplib2.Http, an http object to be used in place of the one the
781 HttpRequest request object was constructed with. If one isn't supplied
782 then use a http object from the requests in this batch.
783
784 Returns:
785 None
786
787 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -0500788 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500789 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -0500790 """
Joe Gregorio654f4a22012-02-09 14:15:44 -0500791
792 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -0500793 if http is None:
794 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500795 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500796 if request is not None:
797 http = request.http
798 break
Joe Gregorio654f4a22012-02-09 14:15:44 -0500799
Joe Gregorio66f57522011-11-30 11:00:00 -0500800 if http is None:
801 raise ValueError("Missing a valid http object.")
802
Joe Gregorio654f4a22012-02-09 14:15:44 -0500803 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500804
Joe Gregorio654f4a22012-02-09 14:15:44 -0500805 # Loop over all the requests and check for 401s. For each 401 request the
806 # credentials should be refreshed and then sent again in a separate batch.
807 redo_requests = {}
808 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -0500809
Joe Gregorio66f57522011-11-30 11:00:00 -0500810 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500811 headers, content = self._responses[request_id]
812 if headers['status'] == '401':
813 redo_order.append(request_id)
814 request = self._requests[request_id]
815 self._refresh_and_apply_credentials(request, http)
816 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -0500817
Joe Gregorio654f4a22012-02-09 14:15:44 -0500818 if redo_requests:
819 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500820
Joe Gregorio654f4a22012-02-09 14:15:44 -0500821 # Now process all callbacks that are erroring, and raise an exception for
822 # ones that return a non-2xx response? Or add extra parameter to callback
823 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -0500824
Joe Gregorio654f4a22012-02-09 14:15:44 -0500825 for request_id in self._order:
826 headers, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500827
Joe Gregorio654f4a22012-02-09 14:15:44 -0500828 request = self._requests[request_id]
829 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500830
Joe Gregorio654f4a22012-02-09 14:15:44 -0500831 response = None
832 exception = None
833 try:
834 r = httplib2.Response(headers)
835 response = request.postproc(r, content)
836 except HttpError, e:
837 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -0500838
Joe Gregorio654f4a22012-02-09 14:15:44 -0500839 if callback is not None:
840 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500841 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500842 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500843
844
Joe Gregorioaf276d22010-12-09 14:26:58 -0500845class HttpRequestMock(object):
846 """Mock of HttpRequest.
847
848 Do not construct directly, instead use RequestMockBuilder.
849 """
850
851 def __init__(self, resp, content, postproc):
852 """Constructor for HttpRequestMock
853
854 Args:
855 resp: httplib2.Response, the response to emulate coming from the request
856 content: string, the response body
857 postproc: callable, the post processing function usually supplied by
858 the model class. See model.JsonModel.response() as an example.
859 """
860 self.resp = resp
861 self.content = content
862 self.postproc = postproc
863 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500864 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500865 if 'reason' in self.resp:
866 self.resp.reason = self.resp['reason']
867
868 def execute(self, http=None):
869 """Execute the request.
870
871 Same behavior as HttpRequest.execute(), but the response is
872 mocked and not really from an HTTP request/response.
873 """
874 return self.postproc(self.resp, self.content)
875
876
877class RequestMockBuilder(object):
878 """A simple mock of HttpRequest
879
880 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400881 tuples of (httplib2.Response, content, opt_expected_body) that should be
882 returned when that method is called. None may also be passed in for the
883 httplib2.Response, in which case a 200 OK response will be generated.
884 If an opt_expected_body (str or dict) is provided, it will be compared to
885 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500886
887 Example:
888 response = '{"data": {"id": "tag:google.c...'
889 requestBuilder = RequestMockBuilder(
890 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500891 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500892 }
893 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500894 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500895
896 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -0500897 200 OK with an empty string as the response content or raise an excpetion
898 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -0400899 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500900
901 For more details see the project wiki.
902 """
903
Joe Gregorioa388ce32011-09-09 17:19:13 -0400904 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500905 """Constructor for RequestMockBuilder
906
907 The constructed object should be a callable object
908 that can replace the class HttpResponse.
909
910 responses - A dictionary that maps methodIds into tuples
911 of (httplib2.Response, content). The methodId
912 comes from the 'rpcName' field in the discovery
913 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400914 check_unexpected - A boolean setting whether or not UnexpectedMethodError
915 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500916 """
917 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400918 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500919
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500920 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500921 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500922 """Implements the callable interface that discovery.build() expects
923 of requestBuilder, which is to build an object compatible with
924 HttpRequest.execute(). See that method for the description of the
925 parameters and the expected response.
926 """
927 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400928 response = self.responses[methodId]
929 resp, content = response[:2]
930 if len(response) > 2:
931 # Test the body against the supplied expected_body.
932 expected_body = response[2]
933 if bool(expected_body) != bool(body):
934 # Not expecting a body and provided one
935 # or expecting a body and not provided one.
936 raise UnexpectedBodyError(expected_body, body)
937 if isinstance(expected_body, str):
938 expected_body = simplejson.loads(expected_body)
939 body = simplejson.loads(body)
940 if body != expected_body:
941 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500942 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400943 elif self.check_unexpected:
944 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500945 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500946 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500947 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500948
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500949
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500950class HttpMock(object):
951 """Mock of httplib2.Http"""
952
Joe Gregorioec343652011-02-16 16:52:51 -0500953 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500954 """
955 Args:
956 filename: string, absolute filename to read response from
957 headers: dict, header to return with response
958 """
Joe Gregorioec343652011-02-16 16:52:51 -0500959 if headers is None:
960 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500961 f = file(filename, 'r')
962 self.data = f.read()
963 f.close()
964 self.headers = headers
965
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500966 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500967 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500968 body=None,
969 headers=None,
970 redirections=1,
971 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500972 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500973
974
975class HttpMockSequence(object):
976 """Mock of httplib2.Http
977
978 Mocks a sequence of calls to request returning different responses for each
979 call. Create an instance initialized with the desired response headers
980 and content and then use as if an httplib2.Http instance.
981
982 http = HttpMockSequence([
983 ({'status': '401'}, ''),
984 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
985 ({'status': '200'}, 'echo_request_headers'),
986 ])
987 resp, content = http.request("http://examples.com")
988
989 There are special values you can pass in for content to trigger
990 behavours that are helpful in testing.
991
992 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400993 'echo_request_headers_as_json' means return the request headers in
994 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500995 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400996 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500997 """
998
999 def __init__(self, iterable):
1000 """
1001 Args:
1002 iterable: iterable, a sequence of pairs of (headers, body)
1003 """
1004 self._iterable = iterable
1005
1006 def request(self, uri,
1007 method='GET',
1008 body=None,
1009 headers=None,
1010 redirections=1,
1011 connection_type=None):
1012 resp, content = self._iterable.pop(0)
1013 if content == 'echo_request_headers':
1014 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001015 elif content == 'echo_request_headers_as_json':
1016 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001017 elif content == 'echo_request_body':
1018 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001019 elif content == 'echo_request_uri':
1020 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001021 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001022
1023
1024def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001025 """Set the user-agent on every request.
1026
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001027 Args:
1028 http - An instance of httplib2.Http
1029 or something that acts like it.
1030 user_agent: string, the value for the user-agent header.
1031
1032 Returns:
1033 A modified instance of http that was passed in.
1034
1035 Example:
1036
1037 h = httplib2.Http()
1038 h = set_user_agent(h, "my-app-name/6.0")
1039
1040 Most of the time the user-agent will be set doing auth, this is for the rare
1041 cases where you are accessing an unauthenticated endpoint.
1042 """
1043 request_orig = http.request
1044
1045 # The closure that will replace 'httplib2.Http.request'.
1046 def new_request(uri, method='GET', body=None, headers=None,
1047 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1048 connection_type=None):
1049 """Modify the request headers to add the user-agent."""
1050 if headers is None:
1051 headers = {}
1052 if 'user-agent' in headers:
1053 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1054 else:
1055 headers['user-agent'] = user_agent
1056 resp, content = request_orig(uri, method, body, headers,
1057 redirections, connection_type)
1058 return resp, content
1059
1060 http.request = new_request
1061 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001062
1063
1064def tunnel_patch(http):
1065 """Tunnel PATCH requests over POST.
1066 Args:
1067 http - An instance of httplib2.Http
1068 or something that acts like it.
1069
1070 Returns:
1071 A modified instance of http that was passed in.
1072
1073 Example:
1074
1075 h = httplib2.Http()
1076 h = tunnel_patch(h, "my-app-name/6.0")
1077
1078 Useful if you are running on a platform that doesn't support PATCH.
1079 Apply this last if you are using OAuth 1.0, as changing the method
1080 will result in a different signature.
1081 """
1082 request_orig = http.request
1083
1084 # The closure that will replace 'httplib2.Http.request'.
1085 def new_request(uri, method='GET', body=None, headers=None,
1086 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1087 connection_type=None):
1088 """Modify the request headers to add the user-agent."""
1089 if headers is None:
1090 headers = {}
1091 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001092 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001093 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001094 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001095 headers['x-http-method-override'] = "PATCH"
1096 method = 'POST'
1097 resp, content = request_orig(uri, method, body, headers,
1098 redirections, connection_type)
1099 return resp, content
1100
1101 http.request = new_request
1102 return http