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