blob: 86d666340d983a33642d9e7845fbc9d6f5859a8d [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
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.
71 """
72
73 def getbytes(self, begin, end):
74 raise NotImplementedError()
75
76 def size(self):
77 raise NotImplementedError()
78
79 def chunksize(self):
80 raise NotImplementedError()
81
82 def mimetype(self):
83 return 'application/octet-stream'
84
85 def resumable(self):
86 return False
87
88 def _to_json(self, strip=None):
89 """Utility function for creating a JSON representation of a MediaUpload.
90
91 Args:
92 strip: array, An array of names of members to not include in the JSON.
93
94 Returns:
95 string, a JSON representation of this instance, suitable to pass to
96 from_json().
97 """
98 t = type(self)
99 d = copy.copy(self.__dict__)
100 if strip is not None:
101 for member in strip:
102 del d[member]
103 d['_class'] = t.__name__
104 d['_module'] = t.__module__
105 return simplejson.dumps(d)
106
107 def to_json(self):
108 """Create a JSON representation of an instance of MediaUpload.
109
110 Returns:
111 string, a JSON representation of this instance, suitable to pass to
112 from_json().
113 """
114 return self._to_json()
115
116 @classmethod
117 def new_from_json(cls, s):
118 """Utility class method to instantiate a MediaUpload subclass from a JSON
119 representation produced by to_json().
120
121 Args:
122 s: string, JSON from to_json().
123
124 Returns:
125 An instance of the subclass of MediaUpload that was serialized with
126 to_json().
127 """
128 data = simplejson.loads(s)
129 # Find and call the right classmethod from_json() to restore the object.
130 module = data['_module']
131 m = __import__(module, fromlist=module.split('.')[:-1])
132 kls = getattr(m, data['_class'])
133 from_json = getattr(kls, 'from_json')
134 return from_json(s)
135
Joe Gregorio66f57522011-11-30 11:00:00 -0500136
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500137class MediaFileUpload(MediaUpload):
138 """A MediaUpload for a file.
139
140 Construct a MediaFileUpload and pass as the media_body parameter of the
141 method. For example, if we had a service that allowed uploading images:
142
143
144 media = MediaFileUpload('smiley.png', mimetype='image/png', chunksize=1000,
145 resumable=True)
146 service.objects().insert(
147 bucket=buckets['items'][0]['id'],
148 name='smiley.png',
149 media_body=media).execute()
150 """
151
Joe Gregorio945be3e2012-01-27 17:01:06 -0500152 def __init__(self, filename, mimetype=None, chunksize=256*1024, resumable=False):
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500153 """Constructor.
154
155 Args:
156 filename: string, Name of the file.
157 mimetype: string, Mime-type of the file. If None then a mime-type will be
158 guessed from the file extension.
159 chunksize: int, File will be uploaded in chunks of this many bytes. Only
160 used if resumable=True.
Joe Gregorio66f57522011-11-30 11:00:00 -0500161 resumable: bool, True if this is a resumable upload. False means upload
162 in a single request.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500163 """
164 self._filename = filename
165 self._size = os.path.getsize(filename)
166 self._fd = None
167 if mimetype is None:
168 (mimetype, encoding) = mimetypes.guess_type(filename)
169 self._mimetype = mimetype
170 self._chunksize = chunksize
171 self._resumable = resumable
172
173 def mimetype(self):
174 return self._mimetype
175
176 def size(self):
177 return self._size
178
179 def chunksize(self):
180 return self._chunksize
181
182 def resumable(self):
183 return self._resumable
184
185 def getbytes(self, begin, length):
186 """Get bytes from the media.
187
188 Args:
189 begin: int, offset from beginning of file.
190 length: int, number of bytes to read, starting at begin.
191
192 Returns:
193 A string of bytes read. May be shorted than length if EOF was reached
194 first.
195 """
196 if self._fd is None:
197 self._fd = open(self._filename, 'rb')
198 self._fd.seek(begin)
199 return self._fd.read(length)
200
201 def to_json(self):
202 """Creating a JSON representation of an instance of Credentials.
203
204 Returns:
205 string, a JSON representation of this instance, suitable to pass to
206 from_json().
207 """
208 return self._to_json(['_fd'])
209
210 @staticmethod
211 def from_json(s):
212 d = simplejson.loads(s)
213 return MediaFileUpload(
214 d['_filename'], d['_mimetype'], d['_chunksize'], d['_resumable'])
215
216
Ali Afshar6f11ea12012-02-07 10:32:14 -0500217class MediaInMemoryUpload(MediaUpload):
218 """MediaUpload for a chunk of bytes.
219
220 Construct a MediaFileUpload and pass as the media_body parameter of the
221 method. For example, if we had a service that allowed plain text:
222 """
223
224 def __init__(self, body, mimetype='application/octet-stream',
225 chunksize=256*1024, resumable=False):
226 """Create a new MediaBytesUpload.
227
228 Args:
229 body: string, Bytes of body content.
230 mimetype: string, Mime-type of the file or default of
231 'application/octet-stream'.
232 chunksize: int, File will be uploaded in chunks of this many bytes. Only
233 used if resumable=True.
234 resumable: bool, True if this is a resumable upload. False means upload
235 in a single request.
236 """
237 self._body = body
238 self._mimetype = mimetype
239 self._resumable = resumable
240 self._chunksize = chunksize
241
242 def chunksize(self):
243 """Chunk size for resumable uploads.
244
245 Returns:
246 Chunk size in bytes.
247 """
248 return self._chunksize
249
250 def mimetype(self):
251 """Mime type of the body.
252
253 Returns:
254 Mime type.
255 """
256 return self._mimetype
257
258 def size(self):
259 """Size of upload.
260
261 Returns:
262 Size of the body.
263 """
Ali Afshar1cb6b672012-03-12 08:46:14 -0400264 return len(self._body)
Ali Afshar6f11ea12012-02-07 10:32:14 -0500265
266 def resumable(self):
267 """Whether this upload is resumable.
268
269 Returns:
270 True if resumable upload or False.
271 """
272 return self._resumable
273
274 def getbytes(self, begin, length):
275 """Get bytes from the media.
276
277 Args:
278 begin: int, offset from beginning of file.
279 length: int, number of bytes to read, starting at begin.
280
281 Returns:
282 A string of bytes read. May be shorter than length if EOF was reached
283 first.
284 """
285 return self._body[begin:begin + length]
286
287 def to_json(self):
288 """Create a JSON representation of a MediaInMemoryUpload.
289
290 Returns:
291 string, a JSON representation of this instance, suitable to pass to
292 from_json().
293 """
294 t = type(self)
295 d = copy.copy(self.__dict__)
296 del d['_body']
297 d['_class'] = t.__name__
298 d['_module'] = t.__module__
299 d['_b64body'] = base64.b64encode(self._body)
300 return simplejson.dumps(d)
301
302 @staticmethod
303 def from_json(s):
304 d = simplejson.loads(s)
305 return MediaInMemoryUpload(base64.b64decode(d['_b64body']),
306 d['_mimetype'], d['_chunksize'],
307 d['_resumable'])
308
309
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400310class HttpRequest(object):
Joe Gregorio66f57522011-11-30 11:00:00 -0500311 """Encapsulates a single HTTP request."""
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400312
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500313 def __init__(self, http, postproc, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500314 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500315 body=None,
316 headers=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500317 methodId=None,
318 resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500319 """Constructor for an HttpRequest.
320
Joe Gregorioaf276d22010-12-09 14:26:58 -0500321 Args:
322 http: httplib2.Http, the transport object to use to make a request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500323 postproc: callable, called on the HTTP response and content to transform
324 it into a data object before returning, or raising an exception
325 on an error.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 uri: string, the absolute URI to send the request to
327 method: string, the HTTP method to use
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500328 body: string, the request body of the HTTP request,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500329 headers: dict, the HTTP request headers
Joe Gregorioaf276d22010-12-09 14:26:58 -0500330 methodId: string, a unique identifier for the API method being called.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500331 resumable: MediaUpload, None if this is not a resumbale request.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500332 """
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400333 self.uri = uri
334 self.method = method
335 self.body = body
336 self.headers = headers or {}
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500337 self.methodId = methodId
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400338 self.http = http
339 self.postproc = postproc
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500340 self.resumable = resumable
341
Joe Gregorio66f57522011-11-30 11:00:00 -0500342 # Pull the multipart boundary out of the content-type header.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500343 major, minor, params = mimeparse.parse_mime_type(
344 headers.get('content-type', 'application/json'))
Joe Gregoriobd512b52011-12-06 15:39:26 -0500345
Joe Gregorio945be3e2012-01-27 17:01:06 -0500346 # The size of the non-media part of the request.
347 self.body_size = len(self.body or '')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500348
349 # The resumable URI to send chunks to.
350 self.resumable_uri = None
351
352 # The bytes that have been uploaded.
353 self.resumable_progress = 0
354
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400355 def execute(self, http=None):
356 """Execute the request.
357
Joe Gregorioaf276d22010-12-09 14:26:58 -0500358 Args:
359 http: httplib2.Http, an http object to be used in place of the
360 one the HttpRequest request object was constructed with.
361
362 Returns:
363 A deserialized object model of the response body as determined
364 by the postproc.
365
366 Raises:
367 apiclient.errors.HttpError if the response was not a 2xx.
368 httplib2.Error if a transport error has occured.
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400369 """
370 if http is None:
371 http = self.http
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500372 if self.resumable:
373 body = None
374 while body is None:
375 _, body = self.next_chunk(http)
376 return body
377 else:
Joe Gregorio884e2b22012-02-24 09:37:00 -0500378 if 'content-length' not in self.headers:
379 self.headers['content-length'] = str(self.body_size)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500380 resp, content = http.request(self.uri, self.method,
381 body=self.body,
382 headers=self.headers)
Joe Gregorio49396552011-03-08 10:39:00 -0500383
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500384 if resp.status >= 300:
385 raise HttpError(resp, content, self.uri)
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400386 return self.postproc(resp, content)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500387
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500388 def next_chunk(self, http=None):
389 """Execute the next step of a resumable upload.
390
Joe Gregorio66f57522011-11-30 11:00:00 -0500391 Can only be used if the method being executed supports media uploads and
392 the MediaUpload object passed in was flagged as using resumable upload.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500393
394 Example:
395
Joe Gregorio66f57522011-11-30 11:00:00 -0500396 media = MediaFileUpload('smiley.png', mimetype='image/png',
397 chunksize=1000, resumable=True)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500398 request = service.objects().insert(
399 bucket=buckets['items'][0]['id'],
400 name='smiley.png',
401 media_body=media)
402
403 response = None
404 while response is None:
405 status, response = request.next_chunk()
406 if status:
407 print "Upload %d%% complete." % int(status.progress() * 100)
408
409
410 Returns:
411 (status, body): (ResumableMediaStatus, object)
412 The body will be None until the resumable media is fully uploaded.
413 """
414 if http is None:
415 http = self.http
416
417 if self.resumable_uri is None:
418 start_headers = copy.copy(self.headers)
419 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
420 start_headers['X-Upload-Content-Length'] = str(self.resumable.size())
Joe Gregorio945be3e2012-01-27 17:01:06 -0500421 start_headers['content-length'] = str(self.body_size)
422
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500423 resp, content = http.request(self.uri, self.method,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500424 body=self.body,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500425 headers=start_headers)
426 if resp.status == 200 and 'location' in resp:
427 self.resumable_uri = resp['location']
428 else:
429 raise ResumableUploadError("Failed to retrieve starting URI.")
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500430
Joe Gregorio945be3e2012-01-27 17:01:06 -0500431 data = self.resumable.getbytes(self.resumable_progress,
432 self.resumable.chunksize())
433
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500434 headers = {
435 'Content-Range': 'bytes %d-%d/%d' % (
436 self.resumable_progress, self.resumable_progress + len(data) - 1,
Joe Gregorio945be3e2012-01-27 17:01:06 -0500437 self.resumable.size()),
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500438 }
439 resp, content = http.request(self.resumable_uri, 'PUT',
440 body=data,
441 headers=headers)
442 if resp.status in [200, 201]:
443 return None, self.postproc(resp, content)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500444 elif resp.status == 308:
Joe Gregorio66f57522011-11-30 11:00:00 -0500445 # A "308 Resume Incomplete" indicates we are not done.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500446 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500447 if 'location' in resp:
448 self.resumable_uri = resp['location']
449 else:
450 raise HttpError(resp, content, self.uri)
451
Joe Gregorio945be3e2012-01-27 17:01:06 -0500452 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
453 None)
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500454
455 def to_json(self):
456 """Returns a JSON representation of the HttpRequest."""
457 d = copy.copy(self.__dict__)
458 if d['resumable'] is not None:
459 d['resumable'] = self.resumable.to_json()
460 del d['http']
461 del d['postproc']
462 return simplejson.dumps(d)
463
464 @staticmethod
465 def from_json(s, http, postproc):
466 """Returns an HttpRequest populated with info from a JSON object."""
467 d = simplejson.loads(s)
468 if d['resumable'] is not None:
469 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
470 return HttpRequest(
471 http,
472 postproc,
Joe Gregorio66f57522011-11-30 11:00:00 -0500473 uri=d['uri'],
474 method=d['method'],
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500475 body=d['body'],
476 headers=d['headers'],
477 methodId=d['methodId'],
478 resumable=d['resumable'])
479
Joe Gregorioaf276d22010-12-09 14:26:58 -0500480
Joe Gregorio66f57522011-11-30 11:00:00 -0500481class BatchHttpRequest(object):
482 """Batches multiple HttpRequest objects into a single HTTP request."""
483
484 def __init__(self, callback=None, batch_uri=None):
485 """Constructor for a BatchHttpRequest.
486
487 Args:
488 callback: callable, A callback to be called for each response, of the
489 form callback(id, response). The first parameter is the request id, and
490 the second is the deserialized response object.
491 batch_uri: string, URI to send batch requests to.
492 """
493 if batch_uri is None:
494 batch_uri = 'https://www.googleapis.com/batch'
495 self._batch_uri = batch_uri
496
497 # Global callback to be called for each individual response in the batch.
498 self._callback = callback
499
Joe Gregorio654f4a22012-02-09 14:15:44 -0500500 # A map from id to request.
Joe Gregorio66f57522011-11-30 11:00:00 -0500501 self._requests = {}
502
Joe Gregorio654f4a22012-02-09 14:15:44 -0500503 # A map from id to callback.
504 self._callbacks = {}
505
Joe Gregorio66f57522011-11-30 11:00:00 -0500506 # List of request ids, in the order in which they were added.
507 self._order = []
508
509 # The last auto generated id.
510 self._last_auto_id = 0
511
512 # Unique ID on which to base the Content-ID headers.
513 self._base_id = None
514
Joe Gregorio654f4a22012-02-09 14:15:44 -0500515 # A map from request id to (headers, content) response pairs
516 self._responses = {}
517
518 # A map of id(Credentials) that have been refreshed.
519 self._refreshed_credentials = {}
520
521 def _refresh_and_apply_credentials(self, request, http):
522 """Refresh the credentials and apply to the request.
523
524 Args:
525 request: HttpRequest, the request.
526 http: httplib2.Http, the global http object for the batch.
527 """
528 # For the credentials to refresh, but only once per refresh_token
529 # If there is no http per the request then refresh the http passed in
530 # via execute()
531 creds = None
532 if request.http is not None and hasattr(request.http.request,
533 'credentials'):
534 creds = request.http.request.credentials
535 elif http is not None and hasattr(http.request, 'credentials'):
536 creds = http.request.credentials
537 if creds is not None:
538 if id(creds) not in self._refreshed_credentials:
539 creds.refresh(http)
540 self._refreshed_credentials[id(creds)] = 1
541
542 # Only apply the credentials if we are using the http object passed in,
543 # otherwise apply() will get called during _serialize_request().
544 if request.http is None or not hasattr(request.http.request,
545 'credentials'):
546 creds.apply(request.headers)
547
Joe Gregorio66f57522011-11-30 11:00:00 -0500548 def _id_to_header(self, id_):
549 """Convert an id to a Content-ID header value.
550
551 Args:
552 id_: string, identifier of individual request.
553
554 Returns:
555 A Content-ID header with the id_ encoded into it. A UUID is prepended to
556 the value because Content-ID headers are supposed to be universally
557 unique.
558 """
559 if self._base_id is None:
560 self._base_id = uuid.uuid4()
561
562 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
563
564 def _header_to_id(self, header):
565 """Convert a Content-ID header value to an id.
566
567 Presumes the Content-ID header conforms to the format that _id_to_header()
568 returns.
569
570 Args:
571 header: string, Content-ID header value.
572
573 Returns:
574 The extracted id value.
575
576 Raises:
577 BatchError if the header is not in the expected format.
578 """
579 if header[0] != '<' or header[-1] != '>':
580 raise BatchError("Invalid value for Content-ID: %s" % header)
581 if '+' not in header:
582 raise BatchError("Invalid value for Content-ID: %s" % header)
583 base, id_ = header[1:-1].rsplit('+', 1)
584
585 return urllib.unquote(id_)
586
587 def _serialize_request(self, request):
588 """Convert an HttpRequest object into a string.
589
590 Args:
591 request: HttpRequest, the request to serialize.
592
593 Returns:
594 The request as a string in application/http format.
595 """
596 # Construct status line
597 parsed = urlparse.urlparse(request.uri)
598 request_line = urlparse.urlunparse(
599 (None, None, parsed.path, parsed.params, parsed.query, None)
600 )
601 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500602 major, minor = request.headers.get('content-type', 'application/json').split('/')
Joe Gregorio66f57522011-11-30 11:00:00 -0500603 msg = MIMENonMultipart(major, minor)
604 headers = request.headers.copy()
605
Joe Gregorio654f4a22012-02-09 14:15:44 -0500606 if request.http is not None and hasattr(request.http.request,
607 'credentials'):
608 request.http.request.credentials.apply(headers)
609
Joe Gregorio66f57522011-11-30 11:00:00 -0500610 # MIMENonMultipart adds its own Content-Type header.
611 if 'content-type' in headers:
612 del headers['content-type']
613
614 for key, value in headers.iteritems():
615 msg[key] = value
616 msg['Host'] = parsed.netloc
617 msg.set_unixfrom(None)
618
619 if request.body is not None:
620 msg.set_payload(request.body)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500621 msg['content-length'] = str(len(request.body))
Joe Gregorio66f57522011-11-30 11:00:00 -0500622
Joe Gregorio654f4a22012-02-09 14:15:44 -0500623 # Serialize the mime message.
624 fp = StringIO.StringIO()
625 # maxheaderlen=0 means don't line wrap headers.
626 g = Generator(fp, maxheaderlen=0)
627 g.flatten(msg, unixfrom=False)
628 body = fp.getvalue()
629
Joe Gregorio66f57522011-11-30 11:00:00 -0500630 # Strip off the \n\n that the MIME lib tacks onto the end of the payload.
631 if request.body is None:
632 body = body[:-2]
633
Joe Gregoriodd813822012-01-25 10:32:47 -0500634 return status_line.encode('utf-8') + body
Joe Gregorio66f57522011-11-30 11:00:00 -0500635
636 def _deserialize_response(self, payload):
637 """Convert string into httplib2 response and content.
638
639 Args:
640 payload: string, headers and body as a string.
641
642 Returns:
643 A pair (resp, content) like would be returned from httplib2.request.
644 """
645 # Strip off the status line
646 status_line, payload = payload.split('\n', 1)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500647 protocol, status, reason = status_line.split(' ', 2)
Joe Gregorio66f57522011-11-30 11:00:00 -0500648
649 # Parse the rest of the response
650 parser = FeedParser()
651 parser.feed(payload)
652 msg = parser.close()
653 msg['status'] = status
654
655 # Create httplib2.Response from the parsed headers.
656 resp = httplib2.Response(msg)
657 resp.reason = reason
658 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
659
660 content = payload.split('\r\n\r\n', 1)[1]
661
662 return resp, content
663
664 def _new_id(self):
665 """Create a new id.
666
667 Auto incrementing number that avoids conflicts with ids already used.
668
669 Returns:
670 string, a new unique id.
671 """
672 self._last_auto_id += 1
673 while str(self._last_auto_id) in self._requests:
674 self._last_auto_id += 1
675 return str(self._last_auto_id)
676
677 def add(self, request, callback=None, request_id=None):
678 """Add a new request.
679
680 Every callback added will be paired with a unique id, the request_id. That
681 unique id will be passed back to the callback when the response comes back
682 from the server. The default behavior is to have the library generate it's
683 own unique id. If the caller passes in a request_id then they must ensure
684 uniqueness for each request_id, and if they are not an exception is
685 raised. Callers should either supply all request_ids or nevery supply a
686 request id, to avoid such an error.
687
688 Args:
689 request: HttpRequest, Request to add to the batch.
690 callback: callable, A callback to be called for this response, of the
691 form callback(id, response). The first parameter is the request id, and
692 the second is the deserialized response object.
693 request_id: string, A unique id for the request. The id will be passed to
694 the callback with the response.
695
696 Returns:
697 None
698
699 Raises:
700 BatchError if a resumable request is added to a batch.
701 KeyError is the request_id is not unique.
702 """
703 if request_id is None:
704 request_id = self._new_id()
705 if request.resumable is not None:
706 raise BatchError("Resumable requests cannot be used in a batch request.")
707 if request_id in self._requests:
708 raise KeyError("A request with this ID already exists: %s" % request_id)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500709 self._requests[request_id] = request
710 self._callbacks[request_id] = callback
Joe Gregorio66f57522011-11-30 11:00:00 -0500711 self._order.append(request_id)
712
Joe Gregorio654f4a22012-02-09 14:15:44 -0500713 def _execute(self, http, order, requests):
714 """Serialize batch request, send to server, process response.
715
716 Args:
717 http: httplib2.Http, an http object to be used to make the request with.
718 order: list, list of request ids in the order they were added to the
719 batch.
720 request: list, list of request objects to send.
721
722 Raises:
723 httplib2.Error if a transport error has occured.
724 apiclient.errors.BatchError if the response is the wrong format.
725 """
726 message = MIMEMultipart('mixed')
727 # Message should not write out it's own headers.
728 setattr(message, '_write_headers', lambda self: None)
729
730 # Add all the individual requests.
731 for request_id in order:
732 request = requests[request_id]
733
734 msg = MIMENonMultipart('application', 'http')
735 msg['Content-Transfer-Encoding'] = 'binary'
736 msg['Content-ID'] = self._id_to_header(request_id)
737
738 body = self._serialize_request(request)
739 msg.set_payload(body)
740 message.attach(msg)
741
742 body = message.as_string()
743
744 headers = {}
745 headers['content-type'] = ('multipart/mixed; '
746 'boundary="%s"') % message.get_boundary()
747
748 resp, content = http.request(self._batch_uri, 'POST', body=body,
749 headers=headers)
750
751 if resp.status >= 300:
752 raise HttpError(resp, content, self._batch_uri)
753
754 # Now break out the individual responses and store each one.
755 boundary, _ = content.split(None, 1)
756
757 # Prepend with a content-type header so FeedParser can handle it.
758 header = 'content-type: %s\r\n\r\n' % resp['content-type']
759 for_parser = header + content
760
761 parser = FeedParser()
762 parser.feed(for_parser)
763 mime_response = parser.close()
764
765 if not mime_response.is_multipart():
766 raise BatchError("Response not in multipart/mixed format.", resp,
767 content)
768
769 for part in mime_response.get_payload():
770 request_id = self._header_to_id(part['Content-ID'])
771 headers, content = self._deserialize_response(part.get_payload())
772 self._responses[request_id] = (headers, content)
773
Joe Gregorio66f57522011-11-30 11:00:00 -0500774 def execute(self, http=None):
775 """Execute all the requests as a single batched HTTP request.
776
777 Args:
778 http: httplib2.Http, an http object to be used in place of the one the
779 HttpRequest request object was constructed with. If one isn't supplied
780 then use a http object from the requests in this batch.
781
782 Returns:
783 None
784
785 Raises:
Joe Gregorio66f57522011-11-30 11:00:00 -0500786 httplib2.Error if a transport error has occured.
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500787 apiclient.errors.BatchError if the response is the wrong format.
Joe Gregorio66f57522011-11-30 11:00:00 -0500788 """
Joe Gregorio654f4a22012-02-09 14:15:44 -0500789
790 # If http is not supplied use the first valid one given in the requests.
Joe Gregorio66f57522011-11-30 11:00:00 -0500791 if http is None:
792 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500793 request = self._requests[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500794 if request is not None:
795 http = request.http
796 break
Joe Gregorio654f4a22012-02-09 14:15:44 -0500797
Joe Gregorio66f57522011-11-30 11:00:00 -0500798 if http is None:
799 raise ValueError("Missing a valid http object.")
800
Joe Gregorio654f4a22012-02-09 14:15:44 -0500801 self._execute(http, self._order, self._requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500802
Joe Gregorio654f4a22012-02-09 14:15:44 -0500803 # Loop over all the requests and check for 401s. For each 401 request the
804 # credentials should be refreshed and then sent again in a separate batch.
805 redo_requests = {}
806 redo_order = []
Joe Gregorio66f57522011-11-30 11:00:00 -0500807
Joe Gregorio66f57522011-11-30 11:00:00 -0500808 for request_id in self._order:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500809 headers, content = self._responses[request_id]
810 if headers['status'] == '401':
811 redo_order.append(request_id)
812 request = self._requests[request_id]
813 self._refresh_and_apply_credentials(request, http)
814 redo_requests[request_id] = request
Joe Gregorio66f57522011-11-30 11:00:00 -0500815
Joe Gregorio654f4a22012-02-09 14:15:44 -0500816 if redo_requests:
817 self._execute(http, redo_order, redo_requests)
Joe Gregorio66f57522011-11-30 11:00:00 -0500818
Joe Gregorio654f4a22012-02-09 14:15:44 -0500819 # Now process all callbacks that are erroring, and raise an exception for
820 # ones that return a non-2xx response? Or add extra parameter to callback
821 # that contains an HttpError?
Joe Gregorio66f57522011-11-30 11:00:00 -0500822
Joe Gregorio654f4a22012-02-09 14:15:44 -0500823 for request_id in self._order:
824 headers, content = self._responses[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500825
Joe Gregorio654f4a22012-02-09 14:15:44 -0500826 request = self._requests[request_id]
827 callback = self._callbacks[request_id]
Joe Gregorio66f57522011-11-30 11:00:00 -0500828
Joe Gregorio654f4a22012-02-09 14:15:44 -0500829 response = None
830 exception = None
831 try:
832 r = httplib2.Response(headers)
833 response = request.postproc(r, content)
834 except HttpError, e:
835 exception = e
Joe Gregorio66f57522011-11-30 11:00:00 -0500836
Joe Gregorio654f4a22012-02-09 14:15:44 -0500837 if callback is not None:
838 callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500839 if self._callback is not None:
Joe Gregorio654f4a22012-02-09 14:15:44 -0500840 self._callback(request_id, response, exception)
Joe Gregorio66f57522011-11-30 11:00:00 -0500841
842
Joe Gregorioaf276d22010-12-09 14:26:58 -0500843class HttpRequestMock(object):
844 """Mock of HttpRequest.
845
846 Do not construct directly, instead use RequestMockBuilder.
847 """
848
849 def __init__(self, resp, content, postproc):
850 """Constructor for HttpRequestMock
851
852 Args:
853 resp: httplib2.Response, the response to emulate coming from the request
854 content: string, the response body
855 postproc: callable, the post processing function usually supplied by
856 the model class. See model.JsonModel.response() as an example.
857 """
858 self.resp = resp
859 self.content = content
860 self.postproc = postproc
861 if resp is None:
Joe Gregorioc6722462010-12-20 14:29:28 -0500862 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
Joe Gregorioaf276d22010-12-09 14:26:58 -0500863 if 'reason' in self.resp:
864 self.resp.reason = self.resp['reason']
865
866 def execute(self, http=None):
867 """Execute the request.
868
869 Same behavior as HttpRequest.execute(), but the response is
870 mocked and not really from an HTTP request/response.
871 """
872 return self.postproc(self.resp, self.content)
873
874
875class RequestMockBuilder(object):
876 """A simple mock of HttpRequest
877
878 Pass in a dictionary to the constructor that maps request methodIds to
Joe Gregorioa388ce32011-09-09 17:19:13 -0400879 tuples of (httplib2.Response, content, opt_expected_body) that should be
880 returned when that method is called. None may also be passed in for the
881 httplib2.Response, in which case a 200 OK response will be generated.
882 If an opt_expected_body (str or dict) is provided, it will be compared to
883 the body and UnexpectedBodyError will be raised on inequality.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500884
885 Example:
886 response = '{"data": {"id": "tag:google.c...'
887 requestBuilder = RequestMockBuilder(
888 {
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500889 'plus.activities.get': (None, response),
Joe Gregorioaf276d22010-12-09 14:26:58 -0500890 }
891 )
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500892 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500893
894 Methods that you do not supply a response for will return a
Joe Gregorio66f57522011-11-30 11:00:00 -0500895 200 OK with an empty string as the response content or raise an excpetion
896 if check_unexpected is set to True. The methodId is taken from the rpcName
Joe Gregorioa388ce32011-09-09 17:19:13 -0400897 in the discovery document.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500898
899 For more details see the project wiki.
900 """
901
Joe Gregorioa388ce32011-09-09 17:19:13 -0400902 def __init__(self, responses, check_unexpected=False):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500903 """Constructor for RequestMockBuilder
904
905 The constructed object should be a callable object
906 that can replace the class HttpResponse.
907
908 responses - A dictionary that maps methodIds into tuples
909 of (httplib2.Response, content). The methodId
910 comes from the 'rpcName' field in the discovery
911 document.
Joe Gregorioa388ce32011-09-09 17:19:13 -0400912 check_unexpected - A boolean setting whether or not UnexpectedMethodError
913 should be raised on unsupplied method.
Joe Gregorioaf276d22010-12-09 14:26:58 -0500914 """
915 self.responses = responses
Joe Gregorioa388ce32011-09-09 17:19:13 -0400916 self.check_unexpected = check_unexpected
Joe Gregorioaf276d22010-12-09 14:26:58 -0500917
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500918 def __call__(self, http, postproc, uri, method='GET', body=None,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500919 headers=None, methodId=None, resumable=None):
Joe Gregorioaf276d22010-12-09 14:26:58 -0500920 """Implements the callable interface that discovery.build() expects
921 of requestBuilder, which is to build an object compatible with
922 HttpRequest.execute(). See that method for the description of the
923 parameters and the expected response.
924 """
925 if methodId in self.responses:
Joe Gregorioa388ce32011-09-09 17:19:13 -0400926 response = self.responses[methodId]
927 resp, content = response[:2]
928 if len(response) > 2:
929 # Test the body against the supplied expected_body.
930 expected_body = response[2]
931 if bool(expected_body) != bool(body):
932 # Not expecting a body and provided one
933 # or expecting a body and not provided one.
934 raise UnexpectedBodyError(expected_body, body)
935 if isinstance(expected_body, str):
936 expected_body = simplejson.loads(expected_body)
937 body = simplejson.loads(body)
938 if body != expected_body:
939 raise UnexpectedBodyError(expected_body, body)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500940 return HttpRequestMock(resp, content, postproc)
Joe Gregorioa388ce32011-09-09 17:19:13 -0400941 elif self.check_unexpected:
942 raise UnexpectedMethodError(methodId)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500943 else:
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500944 model = JsonModel(False)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500945 return HttpRequestMock(None, '{}', model.response)
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500946
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500947
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500948class HttpMock(object):
949 """Mock of httplib2.Http"""
950
Joe Gregorioec343652011-02-16 16:52:51 -0500951 def __init__(self, filename, headers=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500952 """
953 Args:
954 filename: string, absolute filename to read response from
955 headers: dict, header to return with response
956 """
Joe Gregorioec343652011-02-16 16:52:51 -0500957 if headers is None:
958 headers = {'status': '200 OK'}
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500959 f = file(filename, 'r')
960 self.data = f.read()
961 f.close()
962 self.headers = headers
963
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500964 def request(self, uri,
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500965 method='GET',
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500966 body=None,
967 headers=None,
968 redirections=1,
969 connection_type=None):
Joe Gregoriocb8103d2011-02-11 23:20:52 -0500970 return httplib2.Response(self.headers), self.data
Joe Gregorioccc79542011-02-19 00:05:26 -0500971
972
973class HttpMockSequence(object):
974 """Mock of httplib2.Http
975
976 Mocks a sequence of calls to request returning different responses for each
977 call. Create an instance initialized with the desired response headers
978 and content and then use as if an httplib2.Http instance.
979
980 http = HttpMockSequence([
981 ({'status': '401'}, ''),
982 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
983 ({'status': '200'}, 'echo_request_headers'),
984 ])
985 resp, content = http.request("http://examples.com")
986
987 There are special values you can pass in for content to trigger
988 behavours that are helpful in testing.
989
990 'echo_request_headers' means return the request headers in the response body
Joe Gregorioe9e236f2011-03-21 22:23:14 -0400991 'echo_request_headers_as_json' means return the request headers in
992 the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500993 'echo_request_body' means return the request body in the response body
Joe Gregorio0bc70912011-05-24 15:30:49 -0400994 'echo_request_uri' means return the request uri in the response body
Joe Gregorioccc79542011-02-19 00:05:26 -0500995 """
996
997 def __init__(self, iterable):
998 """
999 Args:
1000 iterable: iterable, a sequence of pairs of (headers, body)
1001 """
1002 self._iterable = iterable
1003
1004 def request(self, uri,
1005 method='GET',
1006 body=None,
1007 headers=None,
1008 redirections=1,
1009 connection_type=None):
1010 resp, content = self._iterable.pop(0)
1011 if content == 'echo_request_headers':
1012 content = headers
Joe Gregoriof4153422011-03-18 22:45:18 -04001013 elif content == 'echo_request_headers_as_json':
1014 content = simplejson.dumps(headers)
Joe Gregorioccc79542011-02-19 00:05:26 -05001015 elif content == 'echo_request_body':
1016 content = body
Joe Gregorio0bc70912011-05-24 15:30:49 -04001017 elif content == 'echo_request_uri':
1018 content = uri
Joe Gregorioccc79542011-02-19 00:05:26 -05001019 return httplib2.Response(resp), content
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001020
1021
1022def set_user_agent(http, user_agent):
Joe Gregoriof4153422011-03-18 22:45:18 -04001023 """Set the user-agent on every request.
1024
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001025 Args:
1026 http - An instance of httplib2.Http
1027 or something that acts like it.
1028 user_agent: string, the value for the user-agent header.
1029
1030 Returns:
1031 A modified instance of http that was passed in.
1032
1033 Example:
1034
1035 h = httplib2.Http()
1036 h = set_user_agent(h, "my-app-name/6.0")
1037
1038 Most of the time the user-agent will be set doing auth, this is for the rare
1039 cases where you are accessing an unauthenticated endpoint.
1040 """
1041 request_orig = http.request
1042
1043 # The closure that will replace 'httplib2.Http.request'.
1044 def new_request(uri, method='GET', body=None, headers=None,
1045 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1046 connection_type=None):
1047 """Modify the request headers to add the user-agent."""
1048 if headers is None:
1049 headers = {}
1050 if 'user-agent' in headers:
1051 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1052 else:
1053 headers['user-agent'] = user_agent
1054 resp, content = request_orig(uri, method, body, headers,
1055 redirections, connection_type)
1056 return resp, content
1057
1058 http.request = new_request
1059 return http
Joe Gregoriof4153422011-03-18 22:45:18 -04001060
1061
1062def tunnel_patch(http):
1063 """Tunnel PATCH requests over POST.
1064 Args:
1065 http - An instance of httplib2.Http
1066 or something that acts like it.
1067
1068 Returns:
1069 A modified instance of http that was passed in.
1070
1071 Example:
1072
1073 h = httplib2.Http()
1074 h = tunnel_patch(h, "my-app-name/6.0")
1075
1076 Useful if you are running on a platform that doesn't support PATCH.
1077 Apply this last if you are using OAuth 1.0, as changing the method
1078 will result in a different signature.
1079 """
1080 request_orig = http.request
1081
1082 # The closure that will replace 'httplib2.Http.request'.
1083 def new_request(uri, method='GET', body=None, headers=None,
1084 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1085 connection_type=None):
1086 """Modify the request headers to add the user-agent."""
1087 if headers is None:
1088 headers = {}
1089 if method == 'PATCH':
Joe Gregorio06d852b2011-03-25 15:03:10 -04001090 if 'oauth_token' in headers.get('authorization', ''):
Joe Gregorioe9e236f2011-03-21 22:23:14 -04001091 logging.warning(
Joe Gregorio06d852b2011-03-25 15:03:10 -04001092 'OAuth 1.0 request made with Credentials after tunnel_patch.')
Joe Gregoriof4153422011-03-18 22:45:18 -04001093 headers['x-http-method-override'] = "PATCH"
1094 method = 'POST'
1095 resp, content = request_orig(uri, method, body, headers,
1096 redirections, connection_type)
1097 return resp, content
1098
1099 http.request = new_request
1100 return http