1
2
3
4
5
6
7
8
9
10
11
12
13
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
24 import StringIO
25 import base64
26 import copy
27 import gzip
28 import httplib2
29 import mimeparse
30 import mimetypes
31 import os
32 import urllib
33 import urlparse
34 import uuid
35
36 from email.generator import Generator
37 from email.mime.multipart import MIMEMultipart
38 from email.mime.nonmultipart import MIMENonMultipart
39 from email.parser import FeedParser
40 from errors import BatchError
41 from errors import HttpError
42 from errors import ResumableUploadError
43 from errors import UnexpectedBodyError
44 from errors import UnexpectedMethodError
45 from model import JsonModel
46 from oauth2client.anyjson import simplejson
47
48
49 DEFAULT_CHUNK_SIZE = 512*1024
77
103
210
310
406
499
576
579 """Encapsulates a single HTTP request."""
580
581 - def __init__(self, http, postproc, uri,
582 method='GET',
583 body=None,
584 headers=None,
585 methodId=None,
586 resumable=None):
587 """Constructor for an HttpRequest.
588
589 Args:
590 http: httplib2.Http, the transport object to use to make a request
591 postproc: callable, called on the HTTP response and content to transform
592 it into a data object before returning, or raising an exception
593 on an error.
594 uri: string, the absolute URI to send the request to
595 method: string, the HTTP method to use
596 body: string, the request body of the HTTP request,
597 headers: dict, the HTTP request headers
598 methodId: string, a unique identifier for the API method being called.
599 resumable: MediaUpload, None if this is not a resumbale request.
600 """
601 self.uri = uri
602 self.method = method
603 self.body = body
604 self.headers = headers or {}
605 self.methodId = methodId
606 self.http = http
607 self.postproc = postproc
608 self.resumable = resumable
609 self._in_error_state = False
610
611
612 major, minor, params = mimeparse.parse_mime_type(
613 headers.get('content-type', 'application/json'))
614
615
616 self.body_size = len(self.body or '')
617
618
619 self.resumable_uri = None
620
621
622 self.resumable_progress = 0
623
625 """Execute the request.
626
627 Args:
628 http: httplib2.Http, an http object to be used in place of the
629 one the HttpRequest request object was constructed with.
630
631 Returns:
632 A deserialized object model of the response body as determined
633 by the postproc.
634
635 Raises:
636 apiclient.errors.HttpError if the response was not a 2xx.
637 httplib2.Error if a transport error has occured.
638 """
639 if http is None:
640 http = self.http
641 if self.resumable:
642 body = None
643 while body is None:
644 _, body = self.next_chunk(http)
645 return body
646 else:
647 if 'content-length' not in self.headers:
648 self.headers['content-length'] = str(self.body_size)
649 resp, content = http.request(self.uri, self.method,
650 body=self.body,
651 headers=self.headers)
652
653 if resp.status >= 300:
654 raise HttpError(resp, content, self.uri)
655 return self.postproc(resp, content)
656
658 """Execute the next step of a resumable upload.
659
660 Can only be used if the method being executed supports media uploads and
661 the MediaUpload object passed in was flagged as using resumable upload.
662
663 Example:
664
665 media = MediaFileUpload('cow.png', mimetype='image/png',
666 chunksize=1000, resumable=True)
667 request = farm.animals().insert(
668 id='cow',
669 name='cow.png',
670 media_body=media)
671
672 response = None
673 while response is None:
674 status, response = request.next_chunk()
675 if status:
676 print "Upload %d%% complete." % int(status.progress() * 100)
677
678
679 Returns:
680 (status, body): (ResumableMediaStatus, object)
681 The body will be None until the resumable media is fully uploaded.
682
683 Raises:
684 apiclient.errors.HttpError if the response was not a 2xx.
685 httplib2.Error if a transport error has occured.
686 """
687 if http is None:
688 http = self.http
689
690 if self.resumable.size() is None:
691 size = '*'
692 else:
693 size = str(self.resumable.size())
694
695 if self.resumable_uri is None:
696 start_headers = copy.copy(self.headers)
697 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
698 if size != '*':
699 start_headers['X-Upload-Content-Length'] = size
700 start_headers['content-length'] = str(self.body_size)
701
702 resp, content = http.request(self.uri, self.method,
703 body=self.body,
704 headers=start_headers)
705 if resp.status == 200 and 'location' in resp:
706 self.resumable_uri = resp['location']
707 else:
708 raise ResumableUploadError("Failed to retrieve starting URI.")
709 elif self._in_error_state:
710
711
712
713 headers = {
714 'Content-Range': 'bytes */%s' % size,
715 'content-length': '0'
716 }
717 resp, content = http.request(self.resumable_uri, 'PUT',
718 headers=headers)
719 status, body = self._process_response(resp, content)
720 if body:
721
722 return (status, body)
723
724 data = self.resumable.getbytes(
725 self.resumable_progress, self.resumable.chunksize())
726
727
728 if len(data) < self.resumable.chunksize():
729 size = str(self.resumable_progress + len(data))
730
731 headers = {
732 'Content-Range': 'bytes %d-%d/%s' % (
733 self.resumable_progress, self.resumable_progress + len(data) - 1,
734 size)
735 }
736 try:
737 resp, content = http.request(self.resumable_uri, 'PUT',
738 body=data,
739 headers=headers)
740 except:
741 self._in_error_state = True
742 raise
743
744 return self._process_response(resp, content)
745
747 """Process the response from a single chunk upload.
748
749 Args:
750 resp: httplib2.Response, the response object.
751 content: string, the content of the response.
752
753 Returns:
754 (status, body): (ResumableMediaStatus, object)
755 The body will be None until the resumable media is fully uploaded.
756
757 Raises:
758 apiclient.errors.HttpError if the response was not a 2xx or a 308.
759 """
760 if resp.status in [200, 201]:
761 self._in_error_state = False
762 return None, self.postproc(resp, content)
763 elif resp.status == 308:
764 self._in_error_state = False
765
766 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
767 if 'location' in resp:
768 self.resumable_uri = resp['location']
769 else:
770 self._in_error_state = True
771 raise HttpError(resp, content, self.uri)
772
773 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
774 None)
775
777 """Returns a JSON representation of the HttpRequest."""
778 d = copy.copy(self.__dict__)
779 if d['resumable'] is not None:
780 d['resumable'] = self.resumable.to_json()
781 del d['http']
782 del d['postproc']
783
784 return simplejson.dumps(d)
785
786 @staticmethod
788 """Returns an HttpRequest populated with info from a JSON object."""
789 d = simplejson.loads(s)
790 if d['resumable'] is not None:
791 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
792 return HttpRequest(
793 http,
794 postproc,
795 uri=d['uri'],
796 method=d['method'],
797 body=d['body'],
798 headers=d['headers'],
799 methodId=d['methodId'],
800 resumable=d['resumable'])
801
804 """Batches multiple HttpRequest objects into a single HTTP request.
805
806 Example:
807 from apiclient.http import BatchHttpRequest
808
809 def list_animals(request_id, response, exception):
810 \"\"\"Do something with the animals list response.\"\"\"
811 if exception is not None:
812 # Do something with the exception.
813 pass
814 else:
815 # Do something with the response.
816 pass
817
818 def list_farmers(request_id, response, exception):
819 \"\"\"Do something with the farmers list response.\"\"\"
820 if exception is not None:
821 # Do something with the exception.
822 pass
823 else:
824 # Do something with the response.
825 pass
826
827 service = build('farm', 'v2')
828
829 batch = BatchHttpRequest()
830
831 batch.add(service.animals().list(), list_animals)
832 batch.add(service.farmers().list(), list_farmers)
833 batch.execute(http)
834 """
835
836 - def __init__(self, callback=None, batch_uri=None):
837 """Constructor for a BatchHttpRequest.
838
839 Args:
840 callback: callable, A callback to be called for each response, of the
841 form callback(id, response, exception). The first parameter is the
842 request id, and the second is the deserialized response object. The
843 third is an apiclient.errors.HttpError exception object if an HTTP error
844 occurred while processing the request, or None if no error occurred.
845 batch_uri: string, URI to send batch requests to.
846 """
847 if batch_uri is None:
848 batch_uri = 'https://www.googleapis.com/batch'
849 self._batch_uri = batch_uri
850
851
852 self._callback = callback
853
854
855 self._requests = {}
856
857
858 self._callbacks = {}
859
860
861 self._order = []
862
863
864 self._last_auto_id = 0
865
866
867 self._base_id = None
868
869
870 self._responses = {}
871
872
873 self._refreshed_credentials = {}
874
901
903 """Convert an id to a Content-ID header value.
904
905 Args:
906 id_: string, identifier of individual request.
907
908 Returns:
909 A Content-ID header with the id_ encoded into it. A UUID is prepended to
910 the value because Content-ID headers are supposed to be universally
911 unique.
912 """
913 if self._base_id is None:
914 self._base_id = uuid.uuid4()
915
916 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
917
919 """Convert a Content-ID header value to an id.
920
921 Presumes the Content-ID header conforms to the format that _id_to_header()
922 returns.
923
924 Args:
925 header: string, Content-ID header value.
926
927 Returns:
928 The extracted id value.
929
930 Raises:
931 BatchError if the header is not in the expected format.
932 """
933 if header[0] != '<' or header[-1] != '>':
934 raise BatchError("Invalid value for Content-ID: %s" % header)
935 if '+' not in header:
936 raise BatchError("Invalid value for Content-ID: %s" % header)
937 base, id_ = header[1:-1].rsplit('+', 1)
938
939 return urllib.unquote(id_)
940
942 """Convert an HttpRequest object into a string.
943
944 Args:
945 request: HttpRequest, the request to serialize.
946
947 Returns:
948 The request as a string in application/http format.
949 """
950
951 parsed = urlparse.urlparse(request.uri)
952 request_line = urlparse.urlunparse(
953 (None, None, parsed.path, parsed.params, parsed.query, None)
954 )
955 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
956 major, minor = request.headers.get('content-type', 'application/json').split('/')
957 msg = MIMENonMultipart(major, minor)
958 headers = request.headers.copy()
959
960 if request.http is not None and hasattr(request.http.request,
961 'credentials'):
962 request.http.request.credentials.apply(headers)
963
964
965 if 'content-type' in headers:
966 del headers['content-type']
967
968 for key, value in headers.iteritems():
969 msg[key] = value
970 msg['Host'] = parsed.netloc
971 msg.set_unixfrom(None)
972
973 if request.body is not None:
974 msg.set_payload(request.body)
975 msg['content-length'] = str(len(request.body))
976
977
978 fp = StringIO.StringIO()
979
980 g = Generator(fp, maxheaderlen=0)
981 g.flatten(msg, unixfrom=False)
982 body = fp.getvalue()
983
984
985 if request.body is None:
986 body = body[:-2]
987
988 return status_line.encode('utf-8') + body
989
991 """Convert string into httplib2 response and content.
992
993 Args:
994 payload: string, headers and body as a string.
995
996 Returns:
997 A pair (resp, content), such as would be returned from httplib2.request.
998 """
999
1000 status_line, payload = payload.split('\n', 1)
1001 protocol, status, reason = status_line.split(' ', 2)
1002
1003
1004 parser = FeedParser()
1005 parser.feed(payload)
1006 msg = parser.close()
1007 msg['status'] = status
1008
1009
1010 resp = httplib2.Response(msg)
1011 resp.reason = reason
1012 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1013
1014 content = payload.split('\r\n\r\n', 1)[1]
1015
1016 return resp, content
1017
1019 """Create a new id.
1020
1021 Auto incrementing number that avoids conflicts with ids already used.
1022
1023 Returns:
1024 string, a new unique id.
1025 """
1026 self._last_auto_id += 1
1027 while str(self._last_auto_id) in self._requests:
1028 self._last_auto_id += 1
1029 return str(self._last_auto_id)
1030
1031 - def add(self, request, callback=None, request_id=None):
1032 """Add a new request.
1033
1034 Every callback added will be paired with a unique id, the request_id. That
1035 unique id will be passed back to the callback when the response comes back
1036 from the server. The default behavior is to have the library generate it's
1037 own unique id. If the caller passes in a request_id then they must ensure
1038 uniqueness for each request_id, and if they are not an exception is
1039 raised. Callers should either supply all request_ids or nevery supply a
1040 request id, to avoid such an error.
1041
1042 Args:
1043 request: HttpRequest, Request to add to the batch.
1044 callback: callable, A callback to be called for this response, of the
1045 form callback(id, response, exception). The first parameter is the
1046 request id, and the second is the deserialized response object. The
1047 third is an apiclient.errors.HttpError exception object if an HTTP error
1048 occurred while processing the request, or None if no errors occurred.
1049 request_id: string, A unique id for the request. The id will be passed to
1050 the callback with the response.
1051
1052 Returns:
1053 None
1054
1055 Raises:
1056 BatchError if a media request is added to a batch.
1057 KeyError is the request_id is not unique.
1058 """
1059 if request_id is None:
1060 request_id = self._new_id()
1061 if request.resumable is not None:
1062 raise BatchError("Media requests cannot be used in a batch request.")
1063 if request_id in self._requests:
1064 raise KeyError("A request with this ID already exists: %s" % request_id)
1065 self._requests[request_id] = request
1066 self._callbacks[request_id] = callback
1067 self._order.append(request_id)
1068
1069 - def _execute(self, http, order, requests):
1070 """Serialize batch request, send to server, process response.
1071
1072 Args:
1073 http: httplib2.Http, an http object to be used to make the request with.
1074 order: list, list of request ids in the order they were added to the
1075 batch.
1076 request: list, list of request objects to send.
1077
1078 Raises:
1079 httplib2.Error if a transport error has occured.
1080 apiclient.errors.BatchError if the response is the wrong format.
1081 """
1082 message = MIMEMultipart('mixed')
1083
1084 setattr(message, '_write_headers', lambda self: None)
1085
1086
1087 for request_id in order:
1088 request = requests[request_id]
1089
1090 msg = MIMENonMultipart('application', 'http')
1091 msg['Content-Transfer-Encoding'] = 'binary'
1092 msg['Content-ID'] = self._id_to_header(request_id)
1093
1094 body = self._serialize_request(request)
1095 msg.set_payload(body)
1096 message.attach(msg)
1097
1098 body = message.as_string()
1099
1100 headers = {}
1101 headers['content-type'] = ('multipart/mixed; '
1102 'boundary="%s"') % message.get_boundary()
1103
1104 resp, content = http.request(self._batch_uri, 'POST', body=body,
1105 headers=headers)
1106
1107 if resp.status >= 300:
1108 raise HttpError(resp, content, self._batch_uri)
1109
1110
1111 boundary, _ = content.split(None, 1)
1112
1113
1114 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1115 for_parser = header + content
1116
1117 parser = FeedParser()
1118 parser.feed(for_parser)
1119 mime_response = parser.close()
1120
1121 if not mime_response.is_multipart():
1122 raise BatchError("Response not in multipart/mixed format.", resp,
1123 content)
1124
1125 for part in mime_response.get_payload():
1126 request_id = self._header_to_id(part['Content-ID'])
1127 response, content = self._deserialize_response(part.get_payload())
1128 self._responses[request_id] = (response, content)
1129
1131 """Execute all the requests as a single batched HTTP request.
1132
1133 Args:
1134 http: httplib2.Http, an http object to be used in place of the one the
1135 HttpRequest request object was constructed with. If one isn't supplied
1136 then use a http object from the requests in this batch.
1137
1138 Returns:
1139 None
1140
1141 Raises:
1142 httplib2.Error if a transport error has occured.
1143 apiclient.errors.BatchError if the response is the wrong format.
1144 """
1145
1146
1147 if http is None:
1148 for request_id in self._order:
1149 request = self._requests[request_id]
1150 if request is not None:
1151 http = request.http
1152 break
1153
1154 if http is None:
1155 raise ValueError("Missing a valid http object.")
1156
1157 self._execute(http, self._order, self._requests)
1158
1159
1160
1161 redo_requests = {}
1162 redo_order = []
1163
1164 for request_id in self._order:
1165 resp, content = self._responses[request_id]
1166 if resp['status'] == '401':
1167 redo_order.append(request_id)
1168 request = self._requests[request_id]
1169 self._refresh_and_apply_credentials(request, http)
1170 redo_requests[request_id] = request
1171
1172 if redo_requests:
1173 self._execute(http, redo_order, redo_requests)
1174
1175
1176
1177
1178
1179 for request_id in self._order:
1180 resp, content = self._responses[request_id]
1181
1182 request = self._requests[request_id]
1183 callback = self._callbacks[request_id]
1184
1185 response = None
1186 exception = None
1187 try:
1188 response = request.postproc(resp, content)
1189 except HttpError, e:
1190 exception = e
1191
1192 if callback is not None:
1193 callback(request_id, response, exception)
1194 if self._callback is not None:
1195 self._callback(request_id, response, exception)
1196
1199 """Mock of HttpRequest.
1200
1201 Do not construct directly, instead use RequestMockBuilder.
1202 """
1203
1204 - def __init__(self, resp, content, postproc):
1205 """Constructor for HttpRequestMock
1206
1207 Args:
1208 resp: httplib2.Response, the response to emulate coming from the request
1209 content: string, the response body
1210 postproc: callable, the post processing function usually supplied by
1211 the model class. See model.JsonModel.response() as an example.
1212 """
1213 self.resp = resp
1214 self.content = content
1215 self.postproc = postproc
1216 if resp is None:
1217 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1218 if 'reason' in self.resp:
1219 self.resp.reason = self.resp['reason']
1220
1222 """Execute the request.
1223
1224 Same behavior as HttpRequest.execute(), but the response is
1225 mocked and not really from an HTTP request/response.
1226 """
1227 return self.postproc(self.resp, self.content)
1228
1231 """A simple mock of HttpRequest
1232
1233 Pass in a dictionary to the constructor that maps request methodIds to
1234 tuples of (httplib2.Response, content, opt_expected_body) that should be
1235 returned when that method is called. None may also be passed in for the
1236 httplib2.Response, in which case a 200 OK response will be generated.
1237 If an opt_expected_body (str or dict) is provided, it will be compared to
1238 the body and UnexpectedBodyError will be raised on inequality.
1239
1240 Example:
1241 response = '{"data": {"id": "tag:google.c...'
1242 requestBuilder = RequestMockBuilder(
1243 {
1244 'plus.activities.get': (None, response),
1245 }
1246 )
1247 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1248
1249 Methods that you do not supply a response for will return a
1250 200 OK with an empty string as the response content or raise an excpetion
1251 if check_unexpected is set to True. The methodId is taken from the rpcName
1252 in the discovery document.
1253
1254 For more details see the project wiki.
1255 """
1256
1257 - def __init__(self, responses, check_unexpected=False):
1258 """Constructor for RequestMockBuilder
1259
1260 The constructed object should be a callable object
1261 that can replace the class HttpResponse.
1262
1263 responses - A dictionary that maps methodIds into tuples
1264 of (httplib2.Response, content). The methodId
1265 comes from the 'rpcName' field in the discovery
1266 document.
1267 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1268 should be raised on unsupplied method.
1269 """
1270 self.responses = responses
1271 self.check_unexpected = check_unexpected
1272
1273 - def __call__(self, http, postproc, uri, method='GET', body=None,
1274 headers=None, methodId=None, resumable=None):
1275 """Implements the callable interface that discovery.build() expects
1276 of requestBuilder, which is to build an object compatible with
1277 HttpRequest.execute(). See that method for the description of the
1278 parameters and the expected response.
1279 """
1280 if methodId in self.responses:
1281 response = self.responses[methodId]
1282 resp, content = response[:2]
1283 if len(response) > 2:
1284
1285 expected_body = response[2]
1286 if bool(expected_body) != bool(body):
1287
1288
1289 raise UnexpectedBodyError(expected_body, body)
1290 if isinstance(expected_body, str):
1291 expected_body = simplejson.loads(expected_body)
1292 body = simplejson.loads(body)
1293 if body != expected_body:
1294 raise UnexpectedBodyError(expected_body, body)
1295 return HttpRequestMock(resp, content, postproc)
1296 elif self.check_unexpected:
1297 raise UnexpectedMethodError(methodId)
1298 else:
1299 model = JsonModel(False)
1300 return HttpRequestMock(None, '{}', model.response)
1301
1304 """Mock of httplib2.Http"""
1305
1306 - def __init__(self, filename, headers=None):
1307 """
1308 Args:
1309 filename: string, absolute filename to read response from
1310 headers: dict, header to return with response
1311 """
1312 if headers is None:
1313 headers = {'status': '200 OK'}
1314 f = file(filename, 'r')
1315 self.data = f.read()
1316 f.close()
1317 self.headers = headers
1318
1319 - def request(self, uri,
1320 method='GET',
1321 body=None,
1322 headers=None,
1323 redirections=1,
1324 connection_type=None):
1325 return httplib2.Response(self.headers), self.data
1326
1329 """Mock of httplib2.Http
1330
1331 Mocks a sequence of calls to request returning different responses for each
1332 call. Create an instance initialized with the desired response headers
1333 and content and then use as if an httplib2.Http instance.
1334
1335 http = HttpMockSequence([
1336 ({'status': '401'}, ''),
1337 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1338 ({'status': '200'}, 'echo_request_headers'),
1339 ])
1340 resp, content = http.request("http://examples.com")
1341
1342 There are special values you can pass in for content to trigger
1343 behavours that are helpful in testing.
1344
1345 'echo_request_headers' means return the request headers in the response body
1346 'echo_request_headers_as_json' means return the request headers in
1347 the response body
1348 'echo_request_body' means return the request body in the response body
1349 'echo_request_uri' means return the request uri in the response body
1350 """
1351
1353 """
1354 Args:
1355 iterable: iterable, a sequence of pairs of (headers, body)
1356 """
1357 self._iterable = iterable
1358 self.follow_redirects = True
1359
1360 - def request(self, uri,
1361 method='GET',
1362 body=None,
1363 headers=None,
1364 redirections=1,
1365 connection_type=None):
1366 resp, content = self._iterable.pop(0)
1367 if content == 'echo_request_headers':
1368 content = headers
1369 elif content == 'echo_request_headers_as_json':
1370 content = simplejson.dumps(headers)
1371 elif content == 'echo_request_body':
1372 content = body
1373 elif content == 'echo_request_uri':
1374 content = uri
1375 return httplib2.Response(resp), content
1376
1379 """Set the user-agent on every request.
1380
1381 Args:
1382 http - An instance of httplib2.Http
1383 or something that acts like it.
1384 user_agent: string, the value for the user-agent header.
1385
1386 Returns:
1387 A modified instance of http that was passed in.
1388
1389 Example:
1390
1391 h = httplib2.Http()
1392 h = set_user_agent(h, "my-app-name/6.0")
1393
1394 Most of the time the user-agent will be set doing auth, this is for the rare
1395 cases where you are accessing an unauthenticated endpoint.
1396 """
1397 request_orig = http.request
1398
1399
1400 def new_request(uri, method='GET', body=None, headers=None,
1401 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1402 connection_type=None):
1403 """Modify the request headers to add the user-agent."""
1404 if headers is None:
1405 headers = {}
1406 if 'user-agent' in headers:
1407 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1408 else:
1409 headers['user-agent'] = user_agent
1410 resp, content = request_orig(uri, method, body, headers,
1411 redirections, connection_type)
1412 return resp, content
1413
1414 http.request = new_request
1415 return http
1416
1419 """Tunnel PATCH requests over POST.
1420 Args:
1421 http - An instance of httplib2.Http
1422 or something that acts like it.
1423
1424 Returns:
1425 A modified instance of http that was passed in.
1426
1427 Example:
1428
1429 h = httplib2.Http()
1430 h = tunnel_patch(h, "my-app-name/6.0")
1431
1432 Useful if you are running on a platform that doesn't support PATCH.
1433 Apply this last if you are using OAuth 1.0, as changing the method
1434 will result in a different signature.
1435 """
1436 request_orig = http.request
1437
1438
1439 def new_request(uri, method='GET', body=None, headers=None,
1440 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1441 connection_type=None):
1442 """Modify the request headers to add the user-agent."""
1443 if headers is None:
1444 headers = {}
1445 if method == 'PATCH':
1446 if 'oauth_token' in headers.get('authorization', ''):
1447 logging.warning(
1448 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1449 headers['x-http-method-override'] = "PATCH"
1450 method = 'POST'
1451 resp, content = request_orig(uri, method, body, headers,
1452 redirections, connection_type)
1453 return resp, content
1454
1455 http.request = new_request
1456 return http
1457