Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(193)

Side by Side Diff: appengine/chromium_build_logs/third_party/apiclient/http.py

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: clean up code Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698