blob: 0c49045cf0103b1c232743a4a285ba12a46778e4 [file] [log] [blame]
Joe Gregorio88f699f2012-06-07 13:36:06 -04001# Copyright (C) 2012 Google Inc.
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040014
Joe Gregorioaf276d22010-12-09 14:26:58 -050015"""Classes to encapsulate a single HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040016
Joe Gregorioaf276d22010-12-09 14:26:58 -050017The classes implement a command pattern, with every
18object supporting an execute() method that does the
19actuall HTTP request.
Joe Gregorioc5c5a372010-09-22 11:42:32 -040020"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioaf276d22010-12-09 14:26:58 -050023
Joe Gregorio66f57522011-11-30 11:00:00 -050024import StringIO
Ali Afshar6f11ea12012-02-07 10:32:14 -050025import base64
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import copy
Joe Gregorio66f57522011-11-30 11:00:00 -050027import gzip
Joe Gregorioc6722462010-12-20 14:29:28 -050028import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import mimeparse
30import mimetypes
Joe Gregorio66f57522011-11-30 11:00:00 -050031import os
32import urllib
33import urlparse
34import uuid
Joe Gregoriocb8103d2011-02-11 23:20:52 -050035
Joe Gregorio654f4a22012-02-09 14:15:44 -050036from email.generator import Generator
Joe Gregorio66f57522011-11-30 11:00:00 -050037from email.mime.multipart import MIMEMultipart
38from email.mime.nonmultipart import MIMENonMultipart
39from email.parser import FeedParser
40from errors import BatchError
Joe Gregorio49396552011-03-08 10:39:00 -050041from errors import HttpError
Joe Gregoriod0bd3882011-11-22 09:49:47 -050042from errors import ResumableUploadError
Joe Gregorioa388ce32011-09-09 17:19:13 -040043from errors import UnexpectedBodyError
44from errors import UnexpectedMethodError
Joe Gregorio66f57522011-11-30 11:00:00 -050045from model import JsonModel
Joe Gregorio549230c2012-01-11 10:38:05 -050046from oauth2client.anyjson import simplejson
Joe Gregorioc5c5a372010-09-22 11:42:32 -040047
48
Joe Gregoriod0bd3882011-11-22 09:49:47 -050049class MediaUploadProgress(object):
50 """Status of a resumable upload."""
51
52 def __init__(self, resumable_progress, total_size):
53 """Constructor.
54
55 Args:
56 resumable_progress: int, bytes sent so far.
57 total_size: int, total bytes in complete upload.
58 """
59 self.resumable_progress = resumable_progress
60 self.total_size = total_size
61
62 def progress(self):
63 """Percent of upload completed, as a float."""
Joe Gregorio66f57522011-11-30 11:00:00 -050064 return float(self.resumable_progress) / float(self.total_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -050065
66
67class MediaUpload(object):
68 """Describes a media object to upload.
69
70 Base class that defines the interface of MediaUpload subclasses.
Joe Gregorio88f699f2012-06-07 13:36:06 -040071
72 Note that subclasses of MediaUpload may allow you to control the chunksize
73 when upload a media object. It is important to keep the size of the chunk as
74 large as possible to keep the upload efficient. Other factors may influence
75 the size of the chunk you use, particularly if you are working in an
76 environment where individual HTTP requests may have a hardcoded time limit,
77 such as under certain classes of requests under Google App Engine.
Joe Gregoriod0bd3882011-11-22 09:49:47 -050078 """
79
80 def getbytes(self, begin, end):
81 raise NotImplementedError()
82
83 def size(self):
84 raise NotImplementedError()
85
86 def chunksize(self):
87 raise NotImplementedError()
88
89 def mimetype(self):
90 return 'application/octet-stream'
91
92 def resumable(self):
93 return False
94
95 def _to_json(self, strip=None):
96 """Utility function for creating a JSON representation of a MediaUpload.
97
98 Args:
99 strip: array, An array of names of members to not include in the JSON.
100
101 Returns:
102 string, a JSON representation of this instance, suitable to pass to
103 from_json().
104 """
105 t = type(self)
106 d = copy.copy(self.__dict__)
107 if strip is not None:
108 for member in strip:
109 del d[member]
110 d['_class'] = t.__name__
111 d['_module'] = t.__module__
112 return simplejson.dumps(d)
113
114 def to_json(self):
115 """Create a JSON representation of an instance of MediaUpload.
116
117 Returns:
118 string, a JSON representation of this instance, suitable to pass to
119 from_json().
120 """
121 return self._to_json()
122
123 @classmethod
124 def new_from_json(cls, s):
125 """Utility class method to instantiate a MediaUpload subclass from a JSON
126 representation produced by to_json().
127
128 Args:
129 s: string, JSON from to_json().
130
131 Returns:
132 An instance of the subclass of MediaUpload that was serialized with
133 to_json().
134 """
135 data = simplejson.loads(s)
136 # Find and call the right classmethod from_json() to restore the object.
137 module = data['_module']
138 m = __import__(module, fromlist=module.split('.')[:-1])
139 kls = getattr(m, data['_class'])
140 from_json = getattr(kls, 'from_json')
141 return from_json(s)
142
Joe Gregorio66f57522011-11-30 11:00:00 -0500143
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500144class MediaFileUpload(MediaUpload):
145 """A MediaUpload for a file.
146
147 Construct a MediaFileUpload and pass as the media_body parameter of the
148 method. For example, if we had a service that allowed uploading images:
149
150
151 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
152 resumable=True)
153 service.objects().insert(
154 bucket=buckets['items'][0]['id'],
155 name='smiley.png',
156 media_body=media).execute()
157 """
158
Joe Gregorio945be3e2012-01-27 17:01:06 -0500159 def __init__(self, filename, mimetype=None, chunksize=256*1024, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500160 """Constructor.
161
162 Args:
163 filename: string, Name of the file.
164 mimetype: string, Mime-type of the file. If None then a mime-type will be
165 guessed from the file extension.
166 chunksize: int, File will be uploaded in chunks of this many bytes. Only
167 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500168 resumable: bool, True if this is a resumable upload. False means upload
169 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500170 """
171 self._filename = filename
172 self._size = os.path.getsize(filename)
173 self._fd = None
174 if mimetype is None:
175 (mimetype, encoding) = mimetypes.guess_type(filename)
176 self._mimetype = mimetype
177 self._chunksize = chunksize
178 self._resumable = resumable
179
180 def mimetype(self):
181 return self._mimetype
182
183 def size(self):
184 return self._size
185
186 def chunksize(self):
187 return self._chunksize
188
189 def resumable(self):
190 return self._resumable
191
192 def getbytes(self, begin, length):
193 """Get bytes from the media.
194
195 Args:
196 begin: int, offset from beginning of file.
197 length: int, number of bytes to read, starting at begin.
198
199 Returns:
200 A string of bytes read. May be shorted than length if EOF was reached
201 first.
202 """
203 if self._fd is None:
204 self._fd = open(self._filename, 'rb')
205 self._fd.seek(begin)
206 return self._fd.read(length)
207
208 def to_json(self):
209 """Creating a JSON representation of an instance of Credentials.
210
211 Returns:
212 string, a JSON representation of this instance, suitable to pass to
213 from_json().
214 """
215 return self._to_json(['_fd'])
216
217 @staticmethod
218 def from_json(s):
219 d = simplejson.loads(s)
220 return MediaFileUpload(
221 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
222
223
Ali Afshar6f11ea12012-02-07 10:32:14 -0500224class MediaInMemoryUpload(MediaUpload):
225 """MediaUpload for a chunk of bytes.
226
227 Construct a MediaFileUpload and pass as the media_body parameter of the
228 method. For example, if we had a service that allowed plain text:
229 """
230
231 def __init__(self, body, mimetype='application/octet-stream',
232 chunksize=256*1024, resumable=False):
233 """Create a new MediaBytesUpload.
234
235 Args:
236 body: string, Bytes of body content.
237 mimetype: string, Mime-type of the file or default of
238 'application/octet-stream'.
239 chunksize: int, File will be uploaded in chunks of this many bytes. Only
240 used if resumable=True.
241 resumable: bool, True if this is a resumable upload. False means upload
242 in a single request.
243 """
244 self._body = body
245 self._mimetype = mimetype
246 self._resumable = resumable
247 self._chunksize = chunksize
248
249 def chunksize(self):
250 """Chunk size for resumable uploads.
251
252 Returns:
253 Chunk size in bytes.
254 """
255 return self._chunksize
256
257 def mimetype(self):
258 """Mime type of the body.
259
260 Returns:
261 Mime type.
262 """
263 return self._mimetype
264
265 def size(self):
266 """Size of upload.
267
268 Returns:
269 Size of the body.
270 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400271 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500272
273 def resumable(self):
274 """Whether this upload is resumable.
275
276 Returns:
277 True if resumable upload or False.
278 """
279 return self._resumable
280
281 def getbytes(self, begin, length):
282 """Get bytes from the media.
283
284 Args:
285 begin: int, offset from beginning of file.
286 length: int, number of bytes to read, starting at begin.
287
288 Returns:
289 A string of bytes read. May be shorter than length if EOF was reached
290 first.
291 """
292 return self._body[begin:begin + length]
293
294 def to_json(self):
295 """Create a JSON representation of a MediaInMemoryUpload.
296
297 Returns:
298 string, a JSON representation of this instance, suitable to pass to
299 from_json().
300 """
301 t = type(self)
302 d = copy.copy(self.__dict__)
303 del d['_body']
304 d['_class'] = t.__name__
305 d['_module'] = t.__module__
306 d['_b64body'] = base64.b64encode(self._body)
307 return simplejson.dumps(d)
308
309 @staticmethod
310 def from_json(s):
311 d = simplejson.loads(s)
312 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
313 d['_mimetype'], d['_chunksize'],
314 d['_resumable'])
315
316
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400317class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500318 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400319
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500320 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500321 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500322 body=None,
323 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500324 methodId=None,
325 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 """Constructor for an HttpRequest.
327
Joe Gregorioaf276d22010-12-09 14:26:58 -0500328 Args:
329 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500330 postproc: callable, called on the HTTP response and content to transform
331 it into a data object before returning, or raising an exception
332 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500333 uri: string, the absolute URI to send the request to
334 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500335 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500336 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500337 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500338 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500339 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400340 self.uri = uri
341 self.method = method
342 self.body = body
343 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500344 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400345 self.http = http
346 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500347 self.resumable = resumable
348
Joe Gregorio66f57522011-11-30 11:00:00 -0500349 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500350 major, minor, params = mimeparse.parse_mime_type(
351 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500352
Joe Gregorio945be3e2012-01-27 17:01:06 -0500353 # The size of the non-media part of the request.
354 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500355
356 # The resumable URI to send chunks to.
357 self.resumable_uri = None
358
359 # The bytes that have been uploaded.
360 self.resumable_progress = 0
361
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400362 def execute(self, http=None):
363 """Execute the request.
364
Joe Gregorioaf276d22010-12-09 14:26:58 -0500365 Args:
366 http: httplib2.Http, an http object to be used in place of the
367 one the HttpRequest request object was constructed with.
368
369 Returns:
370 A deserialized object model of the response body as determined
371 by the postproc.
372
373 Raises:
374 apiclient.errors.HttpError if the response was not a 2xx.
375 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400376 """
377 if http is None:
378 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500379 if self.resumable:
380 body = None
381 while body is None:
382 _, body = self.next_chunk(http)
383 return body
384 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500385 if 'content-length' not in self.headers:
386 self.headers['content-length'] = str(self.body_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500387 resp, content = http.request(self.uri, self.method,
388 body=self.body,
389 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500390
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500391 if resp.status >= 300:
392 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400393 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500394
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500395 def next_chunk(self, http=None):
396 """Execute the next step of a resumable upload.
397
Joe Gregorio66f57522011-11-30 11:00:00 -0500398 Can only be used if the method being executed supports media uploads and
399 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500400
401 Example:
402
Joe Gregorio66f57522011-11-30 11:00:00 -0500403 media = MediaFileUpload('smiley.png', mimetype='image/png',
404 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500405 request = service.objects().insert(
406 bucket=buckets['items'][0]['id'],
407 name='smiley.png',
408 media_body=media)
409
410 response = None
411 while response is None:
412 status, response = request.next_chunk()
413 if status:
414 print "Upload %d%% complete." % int(status.progress() * 100)
415
416
417 Returns:
418 (status, body): (ResumableMediaStatus, object)
419 The body will be None until the resumable media is fully uploaded.
420 """
421 if http is None:
422 http = self.http
423
424 if self.resumable_uri is None:
425 start_headers = copy.copy(self.headers)
426 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
427 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
Joe Gregorio945be3e2012-01-27 17:01:06 -0500428 start_headers['content-length'] = str(self.body_size)
429
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500430 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500431 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500432 headers=start_headers)
433 if resp.status == 200 and 'location' in resp:
434 self.resumable_uri = resp['location']
435 else:
436 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500437
Joe Gregorio945be3e2012-01-27 17:01:06 -0500438 data = self.resumable.getbytes(self.resumable_progress,
439 self.resumable.chunksize())
440
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500441 headers = {
442 'Content-Range': 'bytes %d-%d/%d' % (
443 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500444 self.resumable.size()),
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500445 }
446 resp, content = http.request(self.resumable_uri, 'PUT',
447 body=data,
448 headers=headers)
449 if resp.status in [200, 201]:
450 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500451 elif resp.status == 308:
Joe Gregorio66f57522011-11-30 11:00:00 -0500452 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500453 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500454 if 'location' in resp:
455 self.resumable_uri = resp['location']
456 else:
457 raise HttpError(resp, content, self.uri)
458
Joe Gregorio945be3e2012-01-27 17:01:06 -0500459 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
460 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500461
462 def to_json(self):
463 """Returns a JSON representation of the HttpRequest."""
464 d = copy.copy(self.__dict__)
465 if d['resumable'] is not None:
466 d['resumable'] = self.resumable.to_json()
467 del d['http']
468 del d['postproc']
469 return simplejson.dumps(d)
470
471 @staticmethod
472 def from_json(s, http, postproc):
473 """Returns an HttpRequest populated with info from a JSON object."""
474 d = simplejson.loads(s)
475 if d['resumable'] is not None:
476 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
477 return HttpRequest(
478 http,
479 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500480 uri=d['uri'],
481 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500482 body=d['body'],
483 headers=d['headers'],
484 methodId=d['methodId'],
485 resumable=d['resumable'])
486
Joe Gregorioaf276d22010-12-09 14:26:58 -0500487
Joe Gregorio66f57522011-11-30 11:00:00 -0500488class BatchHttpRequest(object):
489 """Batches multiple HttpRequest objects into a single HTTP request."""
490
491 def __init__(self, callback=None, batch_uri=None):
492 """Constructor for a BatchHttpRequest.
493
494 Args:
495 callback: callable, A callback to be called for each response, of the
496 form callback(id, response). The first parameter is the request id, and
497 the second is the deserialized response object.
498 batch_uri: string, URI to send batch requests to.
499 """
500 if batch_uri is None:
501 batch_uri = 'https://www.googleapis.com/batch'
502 self._batch_uri = batch_uri
503
504 # Global callback to be called for each individual response in the batch.
505 self._callback = callback
506
Joe Gregorio654f4a22012-02-09 14:15:44 -0500507 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500508 self._requests = {}
509
Joe Gregorio654f4a22012-02-09 14:15:44 -0500510 # A map from id to callback.
511 self._callbacks = {}
512
Joe Gregorio66f57522011-11-30 11:00:00 -0500513 # List of request ids, in the order in which they were added.
514 self._order = []
515
516 # The last auto generated id.
517 self._last_auto_id = 0
518
519 # Unique ID on which to base the Content-ID headers.
520 self._base_id = None
521
Joe Gregorio654f4a22012-02-09 14:15:44 -0500522 # A map from request id to (headers, content) response pairs
523 self._responses = {}
524
525 # A map of id(Credentials) that have been refreshed.
526 self._refreshed_credentials = {}
527
528 def _refresh_and_apply_credentials(self, request, http):
529 """Refresh the credentials and apply to the request.
530
531 Args:
532 request: HttpRequest, the request.
533 http: httplib2.Http, the global http object for the batch.
534 """
535 # For the credentials to refresh, but only once per refresh_token
536 # If there is no http per the request then refresh the http passed in
537 # via execute()
538 creds = None
539 if request.http is not None and hasattr(request.http.request,
540 'credentials'):
541 creds = request.http.request.credentials
542 elif http is not None and hasattr(http.request, 'credentials'):
543 creds = http.request.credentials
544 if creds is not None:
545 if id(creds) not in self._refreshed_credentials:
546 creds.refresh(http)
547 self._refreshed_credentials[id(creds)] = 1
548
549 # Only apply the credentials if we are using the http object passed in,
550 # otherwise apply() will get called during _serialize_request().
551 if request.http is None or not hasattr(request.http.request,
552 'credentials'):
553 creds.apply(request.headers)
554
Joe Gregorio66f57522011-11-30 11:00:00 -0500555 def _id_to_header(self, id_):
556 """Convert an id to a Content-ID header value.
557
558 Args:
559 id_: string, identifier of individual request.
560
561 Returns:
562 A Content-ID header with the id_ encoded into it. A UUID is prepended to
563 the value because Content-ID headers are supposed to be universally
564 unique.
565 """
566 if self._base_id is None:
567 self._base_id = uuid.uuid4()
568
569 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
570
571 def _header_to_id(self, header):
572 """Convert a Content-ID header value to an id.
573
574 Presumes the Content-ID header conforms to the format that _id_to_header()
575 returns.
576
577 Args:
578 header: string, Content-ID header value.
579
580 Returns:
581 The extracted id value.
582
583 Raises:
584 BatchError if the header is not in the expected format.
585 """
586 if header[0] != '<' or header[-1] != '>':
587 raise BatchError("Invalid value for Content-ID: %s" % header)
588 if '+' not in header:
589 raise BatchError("Invalid value for Content-ID: %s" % header)
590 base, id_ = header[1:-1].rsplit('+', 1)
591
592 return urllib.unquote(id_)
593
594 def _serialize_request(self, request):
595 """Convert an HttpRequest object into a string.
596
597 Args:
598 request: HttpRequest, the request to serialize.
599
600 Returns:
601 The request as a string in application/http format.
602 """
603 # Construct status line
604 parsed = urlparse.urlparse(request.uri)
605 request_line = urlparse.urlunparse(
606 (None, None, parsed.path, parsed.params, parsed.query, None)
607 )
608 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500609 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500610 msg = MIMENonMultipart(major, minor)
611 headers = request.headers.copy()
612
Joe Gregorio654f4a22012-02-09 14:15:44 -0500613 if request.http is not None and hasattr(request.http.request,
614 'credentials'):
615 request.http.request.credentials.apply(headers)
616
Joe Gregorio66f57522011-11-30 11:00:00 -0500617 # MIMENonMultipart adds its own Content-Type header.
618 if 'content-type' in headers:
619 del headers['content-type']
620
621 for key, value in headers.iteritems():
622 msg[key] = value
623 msg['Host'] = parsed.netloc
624 msg.set_unixfrom(None)
625
626 if request.body is not None:
627 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500628 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500629
Joe Gregorio654f4a22012-02-09 14:15:44 -0500630 # Serialize the mime message.
631 fp = StringIO.StringIO()
632 # maxheaderlen=0 means don't line wrap headers.
633 g = Generator(fp, maxheaderlen=0)
634 g.flatten(msg, unixfrom=False)
635 body = fp.getvalue()
636
Joe Gregorio66f57522011-11-30 11:00:00 -0500637 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
638 if request.body is None:
639 body = body[:-2]
640
Joe Gregoriodd813822012-01-25 10:32:47 -0500641 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500642
643 def _deserialize_response(self, payload):
644 """Convert string into httplib2 response and content.
645
646 Args:
647 payload: string, headers and body as a string.
648
649 Returns:
650 A pair (resp, content) like would be returned from httplib2.request.
651 """
652 # Strip off the status line
653 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500654 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500655
656 # Parse the rest of the response
657 parser = FeedParser()
658 parser.feed(payload)
659 msg = parser.close()
660 msg['status'] = status
661
662 # Create httplib2.Response from the parsed headers.
663 resp = httplib2.Response(msg)
664 resp.reason = reason
665 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
666
667 content = payload.split('\r\n\r\n', 1)[1]
668
669 return resp, content
670
671 def _new_id(self):
672 """Create a new id.
673
674 Auto incrementing number that avoids conflicts with ids already used.
675
676 Returns:
677 string, a new unique id.
678 """
679 self._last_auto_id += 1
680 while str(self._last_auto_id) in self._requests:
681 self._last_auto_id += 1
682 return str(self._last_auto_id)
683
684 def add(self, request, callback=None, request_id=None):
685 """Add a new request.
686
687 Every callback added will be paired with a unique id, the request_id. That
688 unique id will be passed back to the callback when the response comes back
689 from the server. The default behavior is to have the library generate it's
690 own unique id. If the caller passes in a request_id then they must ensure
691 uniqueness for each request_id, and if they are not an exception is
692 raised. Callers should either supply all request_ids or nevery supply a
693 request id, to avoid such an error.
694
695 Args:
696 request: HttpRequest, Request to add to the batch.
697 callback: callable, A callback to be called for this response, of the
698 form callback(id, response). The first parameter is the request id, and
699 the second is the deserialized response object.
700 request_id: string, A unique id for the request. The id will be passed to
701 the callback with the response.
702
703 Returns:
704 None
705
706 Raises:
707 BatchError if a resumable request is added to a batch.
708 KeyError is the request_id is not unique.
709 """
710 if request_id is None:
711 request_id = self._new_id()
712 if request.resumable is not None:
713 raise BatchError("Resumable requests cannot be used in a batch request.")
714 if request_id in self._requests:
715 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500716 self._requests[request_id] = request
717 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -0500718 self._order.append(request_id)
719
Joe Gregorio654f4a22012-02-09 14:15:44 -0500720 def _execute(self, http, order, requests):
721 """Serialize batch request, send to server, process response.
722
723 Args:
724 http: httplib2.Http, an http object to be used to make the request with.
725 order: list, list of request ids in the order they were added to the
726 batch.
727 request: list, list of request objects to send.
728
729 Raises:
730 httplib2.Error if a transport error has occured.
731 apiclient.errors.BatchError if the response is the wrong format.
732 """
733 message = MIMEMultipart('mixed')
734 # Message should not write out it's own headers.
735 setattr(message, '_write_headers', lambda self: None)
736
737 # Add all the individual requests.
738 for request_id in order:
739 request = requests[request_id]
740
741 msg = MIMENonMultipart('application', 'http')
742 msg['Content-Transfer-Encoding'] = 'binary'
743 msg['Content-ID'] = self._id_to_header(request_id)
744
745 body = self._serialize_request(request)
746 msg.set_payload(body)
747 message.attach(msg)
748
749 body = message.as_string()
750
751 headers = {}
752 headers['content-type'] = ('multipart/mixed; '
753 'boundary="%s"') % message.get_boundary()
754
755 resp, content = http.request(self._batch_uri, 'POST', body=body,
756 headers=headers)
757
758 if resp.status >= 300:
759 raise HttpError(resp, content, self._batch_uri)
760
761 # Now break out the individual responses and store each one.
762 boundary, _ = content.split(None, 1)
763
764 # Prepend with a content-type header so FeedParser can handle it.
765 header = 'content-type: %s\r\n\r\n' % resp['content-type']
766 for_parser = header + content
767
768 parser = FeedParser()
769 parser.feed(for_parser)
770 mime_response = parser.close()
771
772 if not mime_response.is_multipart():
773 raise BatchError("Response not in multipart/mixed format.", resp,
774 content)
775
776 for part in mime_response.get_payload():
777 request_id = self._header_to_id(part['Content-ID'])
778 headers, content = self._deserialize_response(part.get_payload())
779 self._responses[request_id] = (headers, content)
780
Joe Gregorio66f57522011-11-30 11:00:00 -0500781 def execute(self, http=None):
782 """Execute all the requests as a single batched HTTP request.
783
784 Args:
785 http: httplib2.Http, an http object to be used in place of the one the
786 HttpRequest request object was constructed with. If one isn't supplied
787 then use a http object from the requests in this batch.
788
789 Returns:
790 None
791
792 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -0500793 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500794 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -0500795 """
Joe Gregorio654f4a22012-02-09 14:15:44 -0500796
797 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -0500798 if http is None:
799 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500800 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500801 if request is not None:
802 http = request.http
803 break
Joe Gregorio654f4a22012-02-09 14:15:44 -0500804
Joe Gregorio66f57522011-11-30 11:00:00 -0500805 if http is None:
806 raise ValueError("Missing a valid http object.")
807
Joe Gregorio654f4a22012-02-09 14:15:44 -0500808 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500809
Joe Gregorio654f4a22012-02-09 14:15:44 -0500810 # Loop over all the requests and check for 401s. For each 401 request the
811 # credentials should be refreshed and then sent again in a separate batch.
812 redo_requests = {}
813 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -0500814
Joe Gregorio66f57522011-11-30 11:00:00 -0500815 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500816 headers, content = self._responses[request_id]
817 if headers['status'] == '401':
818 redo_order.append(request_id)
819 request = self._requests[request_id]
820 self._refresh_and_apply_credentials(request, http)
821 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -0500822
Joe Gregorio654f4a22012-02-09 14:15:44 -0500823 if redo_requests:
824 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500825
Joe Gregorio654f4a22012-02-09 14:15:44 -0500826 # Now process all callbacks that are erroring, and raise an exception for
827 # ones that return a non-2xx response? Or add extra parameter to callback
828 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -0500829
Joe Gregorio654f4a22012-02-09 14:15:44 -0500830 for request_id in self._order:
831 headers, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500832
Joe Gregorio654f4a22012-02-09 14:15:44 -0500833 request = self._requests[request_id]
834 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500835
Joe Gregorio654f4a22012-02-09 14:15:44 -0500836 response = None
837 exception = None
838 try:
839 r = httplib2.Response(headers)
840 response = request.postproc(r, content)
841 except HttpError, e:
842 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -0500843
Joe Gregorio654f4a22012-02-09 14:15:44 -0500844 if callback is not None:
845 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500846 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500847 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500848
849
Joe Gregorioaf276d22010-12-09 14:26:58 -0500850class HttpRequestMock(object):
851 """Mock of HttpRequest.
852
853 Do not construct directly, instead use RequestMockBuilder.
854 """
855
856 def __init__(self, resp, content, postproc):
857 """Constructor for HttpRequestMock
858
859 Args:
860 resp: httplib2.Response, the response to emulate coming from the request
861 content: string, the response body
862 postproc: callable, the post processing function usually supplied by
863 the model class. See model.JsonModel.response() as an example.
864 """
865 self.resp = resp
866 self.content = content
867 self.postproc = postproc
868 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500869 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500870 if 'reason' in self.resp:
871 self.resp.reason = self.resp['reason']
872
873 def execute(self, http=None):
874 """Execute the request.
875
876 Same behavior as HttpRequest.execute(), but the response is
877 mocked and not really from an HTTP request/response.
878 """
879 return self.postproc(self.resp, self.content)
880
881
882class RequestMockBuilder(object):
883 """A simple mock of HttpRequest
884
885 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400886 tuples of (httplib2.Response, content, opt_expected_body) that should be
887 returned when that method is called. None may also be passed in for the
888 httplib2.Response, in which case a 200 OK response will be generated.
889 If an opt_expected_body (str or dict) is provided, it will be compared to
890 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500891
892 Example:
893 response = '{"data": {"id": "tag:google.c...'
894 requestBuilder = RequestMockBuilder(
895 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500896 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500897 }
898 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500899 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500900
901 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -0500902 200 OK with an empty string as the response content or raise an excpetion
903 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -0400904 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500905
906 For more details see the project wiki.
907 """
908
Joe Gregorioa388ce32011-09-09 17:19:13 -0400909 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500910 """Constructor for RequestMockBuilder
911
912 The constructed object should be a callable object
913 that can replace the class HttpResponse.
914
915 responses - A dictionary that maps methodIds into tuples
916 of (httplib2.Response, content). The methodId
917 comes from the 'rpcName' field in the discovery
918 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400919 check_unexpected - A boolean setting whether or not UnexpectedMethodError
920 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500921 """
922 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400923 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500924
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500925 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500926 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500927 """Implements the callable interface that discovery.build() expects
928 of requestBuilder, which is to build an object compatible with
929 HttpRequest.execute(). See that method for the description of the
930 parameters and the expected response.
931 """
932 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400933 response = self.responses[methodId]
934 resp, content = response[:2]
935 if len(response) > 2:
936 # Test the body against the supplied expected_body.
937 expected_body = response[2]
938 if bool(expected_body) != bool(body):
939 # Not expecting a body and provided one
940 # or expecting a body and not provided one.
941 raise UnexpectedBodyError(expected_body, body)
942 if isinstance(expected_body, str):
943 expected_body = simplejson.loads(expected_body)
944 body = simplejson.loads(body)
945 if body != expected_body:
946 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500947 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400948 elif self.check_unexpected:
949 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500950 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500951 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500952 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500953
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500954
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500955class HttpMock(object):
956 """Mock of httplib2.Http"""
957
Joe Gregorioec343652011-02-16 16:52:51 -0500958 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500959 """
960 Args:
961 filename: string, absolute filename to read response from
962 headers: dict, header to return with response
963 """
Joe Gregorioec343652011-02-16 16:52:51 -0500964 if headers is None:
965 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500966 f = file(filename, 'r')
967 self.data = f.read()
968 f.close()
969 self.headers = headers
970
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500971 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500972 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500973 body=None,
974 headers=None,
975 redirections=1,
976 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500977 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500978
979
980class HttpMockSequence(object):
981 """Mock of httplib2.Http
982
983 Mocks a sequence of calls to request returning different responses for each
984 call. Create an instance initialized with the desired response headers
985 and content and then use as if an httplib2.Http instance.
986
987 http = HttpMockSequence([
988 ({'status': '401'}, ''),
989 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
990 ({'status': '200'}, 'echo_request_headers'),
991 ])
992 resp, content = http.request("http://examples.com")
993
994 There are special values you can pass in for content to trigger
995 behavours that are helpful in testing.
996
997 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400998 'echo_request_headers_as_json' means return the request headers in
999 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001000 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001001 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -05001002 """
1003
1004 def __init__(self, iterable):
1005 """
1006 Args:
1007 iterable: iterable, a sequence of pairs of (headers, body)
1008 """
1009 self._iterable = iterable
1010
1011 def request(self, uri,
1012 method='GET',
1013 body=None,
1014 headers=None,
1015 redirections=1,
1016 connection_type=None):
1017 resp, content = self._iterable.pop(0)
1018 if content == 'echo_request_headers':
1019 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001020 elif content == 'echo_request_headers_as_json':
1021 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001022 elif content == 'echo_request_body':
1023 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001024 elif content == 'echo_request_uri':
1025 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001026 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001027
1028
1029def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001030 """Set the user-agent on every request.
1031
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001032 Args:
1033 http - An instance of httplib2.Http
1034 or something that acts like it.
1035 user_agent: string, the value for the user-agent header.
1036
1037 Returns:
1038 A modified instance of http that was passed in.
1039
1040 Example:
1041
1042 h = httplib2.Http()
1043 h = set_user_agent(h, "my-app-name/6.0")
1044
1045 Most of the time the user-agent will be set doing auth, this is for the rare
1046 cases where you are accessing an unauthenticated endpoint.
1047 """
1048 request_orig = http.request
1049
1050 # The closure that will replace 'httplib2.Http.request'.
1051 def new_request(uri, method='GET', body=None, headers=None,
1052 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1053 connection_type=None):
1054 """Modify the request headers to add the user-agent."""
1055 if headers is None:
1056 headers = {}
1057 if 'user-agent' in headers:
1058 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1059 else:
1060 headers['user-agent'] = user_agent
1061 resp, content = request_orig(uri, method, body, headers,
1062 redirections, connection_type)
1063 return resp, content
1064
1065 http.request = new_request
1066 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001067
1068
1069def tunnel_patch(http):
1070 """Tunnel PATCH requests over POST.
1071 Args:
1072 http - An instance of httplib2.Http
1073 or something that acts like it.
1074
1075 Returns:
1076 A modified instance of http that was passed in.
1077
1078 Example:
1079
1080 h = httplib2.Http()
1081 h = tunnel_patch(h, "my-app-name/6.0")
1082
1083 Useful if you are running on a platform that doesn't support PATCH.
1084 Apply this last if you are using OAuth 1.0, as changing the method
1085 will result in a different signature.
1086 """
1087 request_orig = http.request
1088
1089 # The closure that will replace 'httplib2.Http.request'.
1090 def new_request(uri, method='GET', body=None, headers=None,
1091 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1092 connection_type=None):
1093 """Modify the request headers to add the user-agent."""
1094 if headers is None:
1095 headers = {}
1096 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001097 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001098 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001099 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001100 headers['x-http-method-override'] = "PATCH"
1101 method = 'POST'
1102 resp, content = request_orig(uri, method, body, headers,
1103 redirections, connection_type)
1104 return resp, content
1105
1106 http.request = new_request
1107 return http