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 from __future__ import absolute_import
22 import six
23 from six.moves import range
24
25 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
26
27 from six import BytesIO, StringIO
28 from six.moves.urllib.parse import urlparse, urlunparse, quote, unquote
29
30 import base64
31 import copy
32 import gzip
33 import httplib2
34 import json
35 import logging
36 import mimetypes
37 import os
38 import random
39 import sys
40 import time
41 import uuid
42
43 from email.generator import Generator
44 from email.mime.multipart import MIMEMultipart
45 from email.mime.nonmultipart import MIMENonMultipart
46 from email.parser import FeedParser
47
48 from googleapiclient import mimeparse
49 from googleapiclient.errors import BatchError
50 from googleapiclient.errors import HttpError
51 from googleapiclient.errors import InvalidChunkSizeError
52 from googleapiclient.errors import ResumableUploadError
53 from googleapiclient.errors import UnexpectedBodyError
54 from googleapiclient.errors import UnexpectedMethodError
55 from googleapiclient.model import JsonModel
56 from oauth2client import util
57
58
59 DEFAULT_CHUNK_SIZE = 512*1024
60
61 MAX_URI_LENGTH = 2048
89
115
258
383
446
475
574
577 """Truncated stream.
578
579 Takes a stream and presents a stream that is a slice of the original stream.
580 This is used when uploading media in chunks. In later versions of Python a
581 stream can be passed to httplib in place of the string of data to send. The
582 problem is that httplib just blindly reads to the end of the stream. This
583 wrapper presents a virtual stream that only reads to the end of the chunk.
584 """
585
586 - def __init__(self, stream, begin, chunksize):
587 """Constructor.
588
589 Args:
590 stream: (io.Base, file object), the stream to wrap.
591 begin: int, the seek position the chunk begins at.
592 chunksize: int, the size of the chunk.
593 """
594 self._stream = stream
595 self._begin = begin
596 self._chunksize = chunksize
597 self._stream.seek(begin)
598
599 - def read(self, n=-1):
600 """Read n bytes.
601
602 Args:
603 n, int, the number of bytes to read.
604
605 Returns:
606 A string of length 'n', or less if EOF is reached.
607 """
608
609 cur = self._stream.tell()
610 end = self._begin + self._chunksize
611 if n == -1 or cur + n > end:
612 n = end - cur
613 return self._stream.read(n)
614
617 """Encapsulates a single HTTP request."""
618
619 @util.positional(4)
620 - def __init__(self, http, postproc, uri,
621 method='GET',
622 body=None,
623 headers=None,
624 methodId=None,
625 resumable=None):
626 """Constructor for an HttpRequest.
627
628 Args:
629 http: httplib2.Http, the transport object to use to make a request
630 postproc: callable, called on the HTTP response and content to transform
631 it into a data object before returning, or raising an exception
632 on an error.
633 uri: string, the absolute URI to send the request to
634 method: string, the HTTP method to use
635 body: string, the request body of the HTTP request,
636 headers: dict, the HTTP request headers
637 methodId: string, a unique identifier for the API method being called.
638 resumable: MediaUpload, None if this is not a resumbale request.
639 """
640 self.uri = uri
641 self.method = method
642 self.body = body
643 self.headers = headers or {}
644 self.methodId = methodId
645 self.http = http
646 self.postproc = postproc
647 self.resumable = resumable
648 self.response_callbacks = []
649 self._in_error_state = False
650
651
652 major, minor, params = mimeparse.parse_mime_type(
653 headers.get('content-type', 'application/json'))
654
655
656 self.body_size = len(self.body or '')
657
658
659 self.resumable_uri = None
660
661
662 self.resumable_progress = 0
663
664
665 self._rand = random.random
666 self._sleep = time.sleep
667
668 @util.positional(1)
669 - def execute(self, http=None, num_retries=0):
670 """Execute the request.
671
672 Args:
673 http: httplib2.Http, an http object to be used in place of the
674 one the HttpRequest request object was constructed with.
675 num_retries: Integer, number of times to retry 500's with randomized
676 exponential backoff. If all retries fail, the raised HttpError
677 represents the last request. If zero (default), we attempt the
678 request only once.
679
680 Returns:
681 A deserialized object model of the response body as determined
682 by the postproc.
683
684 Raises:
685 googleapiclient.errors.HttpError if the response was not a 2xx.
686 httplib2.HttpLib2Error if a transport error has occured.
687 """
688 if http is None:
689 http = self.http
690
691 if self.resumable:
692 body = None
693 while body is None:
694 _, body = self.next_chunk(http=http, num_retries=num_retries)
695 return body
696
697
698
699 if 'content-length' not in self.headers:
700 self.headers['content-length'] = str(self.body_size)
701
702 if len(self.uri) > MAX_URI_LENGTH and self.method == 'GET':
703 self.method = 'POST'
704 self.headers['x-http-method-override'] = 'GET'
705 self.headers['content-type'] = 'application/x-www-form-urlencoded'
706 parsed = urlparse(self.uri)
707 self.uri = urlunparse(
708 (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None,
709 None)
710 )
711 self.body = parsed.query
712 self.headers['content-length'] = str(len(self.body))
713
714
715 for retry_num in range(num_retries + 1):
716 if retry_num > 0:
717 self._sleep(self._rand() * 2**retry_num)
718 logging.warning('Retry #%d for request: %s %s, following status: %d'
719 % (retry_num, self.method, self.uri, resp.status))
720
721 resp, content = http.request(str(self.uri), method=str(self.method),
722 body=self.body, headers=self.headers)
723 if resp.status < 500:
724 break
725
726 for callback in self.response_callbacks:
727 callback(resp)
728 if resp.status >= 300:
729 raise HttpError(resp, content, uri=self.uri)
730 return self.postproc(resp, content)
731
732 @util.positional(2)
734 """add_response_headers_callback
735
736 Args:
737 cb: Callback to be called on receiving the response headers, of signature:
738
739 def cb(resp):
740 # Where resp is an instance of httplib2.Response
741 """
742 self.response_callbacks.append(cb)
743
744 @util.positional(1)
746 """Execute the next step of a resumable upload.
747
748 Can only be used if the method being executed supports media uploads and
749 the MediaUpload object passed in was flagged as using resumable upload.
750
751 Example:
752
753 media = MediaFileUpload('cow.png', mimetype='image/png',
754 chunksize=1000, resumable=True)
755 request = farm.animals().insert(
756 id='cow',
757 name='cow.png',
758 media_body=media)
759
760 response = None
761 while response is None:
762 status, response = request.next_chunk()
763 if status:
764 print "Upload %d%% complete." % int(status.progress() * 100)
765
766
767 Args:
768 http: httplib2.Http, an http object to be used in place of the
769 one the HttpRequest request object was constructed with.
770 num_retries: Integer, number of times to retry 500's with randomized
771 exponential backoff. If all retries fail, the raised HttpError
772 represents the last request. If zero (default), we attempt the
773 request only once.
774
775 Returns:
776 (status, body): (ResumableMediaStatus, object)
777 The body will be None until the resumable media is fully uploaded.
778
779 Raises:
780 googleapiclient.errors.HttpError if the response was not a 2xx.
781 httplib2.HttpLib2Error if a transport error has occured.
782 """
783 if http is None:
784 http = self.http
785
786 if self.resumable.size() is None:
787 size = '*'
788 else:
789 size = str(self.resumable.size())
790
791 if self.resumable_uri is None:
792 start_headers = copy.copy(self.headers)
793 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
794 if size != '*':
795 start_headers['X-Upload-Content-Length'] = size
796 start_headers['content-length'] = str(self.body_size)
797
798 for retry_num in range(num_retries + 1):
799 if retry_num > 0:
800 self._sleep(self._rand() * 2**retry_num)
801 logging.warning(
802 'Retry #%d for resumable URI request: %s %s, following status: %d'
803 % (retry_num, self.method, self.uri, resp.status))
804
805 resp, content = http.request(self.uri, method=self.method,
806 body=self.body,
807 headers=start_headers)
808 if resp.status < 500:
809 break
810
811 if resp.status == 200 and 'location' in resp:
812 self.resumable_uri = resp['location']
813 else:
814 raise ResumableUploadError(resp, content)
815 elif self._in_error_state:
816
817
818
819 headers = {
820 'Content-Range': 'bytes */%s' % size,
821 'content-length': '0'
822 }
823 resp, content = http.request(self.resumable_uri, 'PUT',
824 headers=headers)
825 status, body = self._process_response(resp, content)
826 if body:
827
828 return (status, body)
829
830
831
832
833 if self.resumable.has_stream() and sys.version_info[1] >= 6:
834 data = self.resumable.stream()
835 if self.resumable.chunksize() == -1:
836 data.seek(self.resumable_progress)
837 chunk_end = self.resumable.size() - self.resumable_progress - 1
838 else:
839
840 data = _StreamSlice(data, self.resumable_progress,
841 self.resumable.chunksize())
842 chunk_end = min(
843 self.resumable_progress + self.resumable.chunksize() - 1,
844 self.resumable.size() - 1)
845 else:
846 data = self.resumable.getbytes(
847 self.resumable_progress, self.resumable.chunksize())
848
849
850 if len(data) < self.resumable.chunksize():
851 size = str(self.resumable_progress + len(data))
852
853 chunk_end = self.resumable_progress + len(data) - 1
854
855 headers = {
856 'Content-Range': 'bytes %d-%d/%s' % (
857 self.resumable_progress, chunk_end, size),
858
859
860 'Content-Length': str(chunk_end - self.resumable_progress + 1)
861 }
862
863 for retry_num in range(num_retries + 1):
864 if retry_num > 0:
865 self._sleep(self._rand() * 2**retry_num)
866 logging.warning(
867 'Retry #%d for media upload: %s %s, following status: %d'
868 % (retry_num, self.method, self.uri, resp.status))
869
870 try:
871 resp, content = http.request(self.resumable_uri, method='PUT',
872 body=data,
873 headers=headers)
874 except:
875 self._in_error_state = True
876 raise
877 if resp.status < 500:
878 break
879
880 return self._process_response(resp, content)
881
883 """Process the response from a single chunk upload.
884
885 Args:
886 resp: httplib2.Response, the response object.
887 content: string, the content of the response.
888
889 Returns:
890 (status, body): (ResumableMediaStatus, object)
891 The body will be None until the resumable media is fully uploaded.
892
893 Raises:
894 googleapiclient.errors.HttpError if the response was not a 2xx or a 308.
895 """
896 if resp.status in [200, 201]:
897 self._in_error_state = False
898 return None, self.postproc(resp, content)
899 elif resp.status == 308:
900 self._in_error_state = False
901
902 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
903 if 'location' in resp:
904 self.resumable_uri = resp['location']
905 else:
906 self._in_error_state = True
907 raise HttpError(resp, content, uri=self.uri)
908
909 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
910 None)
911
913 """Returns a JSON representation of the HttpRequest."""
914 d = copy.copy(self.__dict__)
915 if d['resumable'] is not None:
916 d['resumable'] = self.resumable.to_json()
917 del d['http']
918 del d['postproc']
919 del d['_sleep']
920 del d['_rand']
921
922 return json.dumps(d)
923
924 @staticmethod
926 """Returns an HttpRequest populated with info from a JSON object."""
927 d = json.loads(s)
928 if d['resumable'] is not None:
929 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
930 return HttpRequest(
931 http,
932 postproc,
933 uri=d['uri'],
934 method=d['method'],
935 body=d['body'],
936 headers=d['headers'],
937 methodId=d['methodId'],
938 resumable=d['resumable'])
939
942 """Batches multiple HttpRequest objects into a single HTTP request.
943
944 Example:
945 from googleapiclient.http import BatchHttpRequest
946
947 def list_animals(request_id, response, exception):
948 \"\"\"Do something with the animals list response.\"\"\"
949 if exception is not None:
950 # Do something with the exception.
951 pass
952 else:
953 # Do something with the response.
954 pass
955
956 def list_farmers(request_id, response, exception):
957 \"\"\"Do something with the farmers list response.\"\"\"
958 if exception is not None:
959 # Do something with the exception.
960 pass
961 else:
962 # Do something with the response.
963 pass
964
965 service = build('farm', 'v2')
966
967 batch = BatchHttpRequest()
968
969 batch.add(service.animals().list(), list_animals)
970 batch.add(service.farmers().list(), list_farmers)
971 batch.execute(http=http)
972 """
973
974 @util.positional(1)
975 - def __init__(self, callback=None, batch_uri=None):
976 """Constructor for a BatchHttpRequest.
977
978 Args:
979 callback: callable, A callback to be called for each response, of the
980 form callback(id, response, exception). The first parameter is the
981 request id, and the second is the deserialized response object. The
982 third is an googleapiclient.errors.HttpError exception object if an HTTP error
983 occurred while processing the request, or None if no error occurred.
984 batch_uri: string, URI to send batch requests to.
985 """
986 if batch_uri is None:
987 batch_uri = 'https://www.googleapis.com/batch'
988 self._batch_uri = batch_uri
989
990
991 self._callback = callback
992
993
994 self._requests = {}
995
996
997 self._callbacks = {}
998
999
1000 self._order = []
1001
1002
1003 self._last_auto_id = 0
1004
1005
1006 self._base_id = None
1007
1008
1009 self._responses = {}
1010
1011
1012 self._refreshed_credentials = {}
1013
1015 """Refresh the credentials and apply to the request.
1016
1017 Args:
1018 request: HttpRequest, the request.
1019 http: httplib2.Http, the global http object for the batch.
1020 """
1021
1022
1023
1024 creds = None
1025 if request.http is not None and hasattr(request.http.request,
1026 'credentials'):
1027 creds = request.http.request.credentials
1028 elif http is not None and hasattr(http.request, 'credentials'):
1029 creds = http.request.credentials
1030 if creds is not None:
1031 if id(creds) not in self._refreshed_credentials:
1032 creds.refresh(http)
1033 self._refreshed_credentials[id(creds)] = 1
1034
1035
1036
1037 if request.http is None or not hasattr(request.http.request,
1038 'credentials'):
1039 creds.apply(request.headers)
1040
1042 """Convert an id to a Content-ID header value.
1043
1044 Args:
1045 id_: string, identifier of individual request.
1046
1047 Returns:
1048 A Content-ID header with the id_ encoded into it. A UUID is prepended to
1049 the value because Content-ID headers are supposed to be universally
1050 unique.
1051 """
1052 if self._base_id is None:
1053 self._base_id = uuid.uuid4()
1054
1055 return '<%s+%s>' % (self._base_id, quote(id_))
1056
1058 """Convert a Content-ID header value to an id.
1059
1060 Presumes the Content-ID header conforms to the format that _id_to_header()
1061 returns.
1062
1063 Args:
1064 header: string, Content-ID header value.
1065
1066 Returns:
1067 The extracted id value.
1068
1069 Raises:
1070 BatchError if the header is not in the expected format.
1071 """
1072 if header[0] != '<' or header[-1] != '>':
1073 raise BatchError("Invalid value for Content-ID: %s" % header)
1074 if '+' not in header:
1075 raise BatchError("Invalid value for Content-ID: %s" % header)
1076 base, id_ = header[1:-1].rsplit('+', 1)
1077
1078 return unquote(id_)
1079
1081 """Convert an HttpRequest object into a string.
1082
1083 Args:
1084 request: HttpRequest, the request to serialize.
1085
1086 Returns:
1087 The request as a string in application/http format.
1088 """
1089
1090 parsed = urlparse(request.uri)
1091 request_line = urlunparse(
1092 ('', '', parsed.path, parsed.params, parsed.query, '')
1093 )
1094 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
1095 major, minor = request.headers.get('content-type', 'application/json').split('/')
1096 msg = MIMENonMultipart(major, minor)
1097 headers = request.headers.copy()
1098
1099 if request.http is not None and hasattr(request.http.request,
1100 'credentials'):
1101 request.http.request.credentials.apply(headers)
1102
1103
1104 if 'content-type' in headers:
1105 del headers['content-type']
1106
1107 for key, value in six.iteritems(headers):
1108 msg[key] = value
1109 msg['Host'] = parsed.netloc
1110 msg.set_unixfrom(None)
1111
1112 if request.body is not None:
1113 msg.set_payload(request.body)
1114 msg['content-length'] = str(len(request.body))
1115
1116
1117 fp = StringIO()
1118
1119 g = Generator(fp, maxheaderlen=0)
1120 g.flatten(msg, unixfrom=False)
1121 body = fp.getvalue()
1122
1123
1124 if request.body is None:
1125 body = body[:-2]
1126
1127 return status_line + body
1128
1130 """Convert string into httplib2 response and content.
1131
1132 Args:
1133 payload: string, headers and body as a string.
1134
1135 Returns:
1136 A pair (resp, content), such as would be returned from httplib2.request.
1137 """
1138
1139 status_line, payload = payload.split('\n', 1)
1140 protocol, status, reason = status_line.split(' ', 2)
1141
1142
1143 parser = FeedParser()
1144 parser.feed(payload)
1145 msg = parser.close()
1146 msg['status'] = status
1147
1148
1149 resp = httplib2.Response(msg)
1150 resp.reason = reason
1151 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1152
1153 content = payload.split('\r\n\r\n', 1)[1]
1154
1155 return resp, content
1156
1158 """Create a new id.
1159
1160 Auto incrementing number that avoids conflicts with ids already used.
1161
1162 Returns:
1163 string, a new unique id.
1164 """
1165 self._last_auto_id += 1
1166 while str(self._last_auto_id) in self._requests:
1167 self._last_auto_id += 1
1168 return str(self._last_auto_id)
1169
1170 @util.positional(2)
1171 - def add(self, request, callback=None, request_id=None):
1172 """Add a new request.
1173
1174 Every callback added will be paired with a unique id, the request_id. That
1175 unique id will be passed back to the callback when the response comes back
1176 from the server. The default behavior is to have the library generate it's
1177 own unique id. If the caller passes in a request_id then they must ensure
1178 uniqueness for each request_id, and if they are not an exception is
1179 raised. Callers should either supply all request_ids or nevery supply a
1180 request id, to avoid such an error.
1181
1182 Args:
1183 request: HttpRequest, Request to add to the batch.
1184 callback: callable, A callback to be called for this response, of the
1185 form callback(id, response, exception). The first parameter is the
1186 request id, and the second is the deserialized response object. The
1187 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1188 occurred while processing the request, or None if no errors occurred.
1189 request_id: string, A unique id for the request. The id will be passed to
1190 the callback with the response.
1191
1192 Returns:
1193 None
1194
1195 Raises:
1196 BatchError if a media request is added to a batch.
1197 KeyError is the request_id is not unique.
1198 """
1199 if request_id is None:
1200 request_id = self._new_id()
1201 if request.resumable is not None:
1202 raise BatchError("Media requests cannot be used in a batch request.")
1203 if request_id in self._requests:
1204 raise KeyError("A request with this ID already exists: %s" % request_id)
1205 self._requests[request_id] = request
1206 self._callbacks[request_id] = callback
1207 self._order.append(request_id)
1208
1209 - def _execute(self, http, order, requests):
1210 """Serialize batch request, send to server, process response.
1211
1212 Args:
1213 http: httplib2.Http, an http object to be used to make the request with.
1214 order: list, list of request ids in the order they were added to the
1215 batch.
1216 request: list, list of request objects to send.
1217
1218 Raises:
1219 httplib2.HttpLib2Error if a transport error has occured.
1220 googleapiclient.errors.BatchError if the response is the wrong format.
1221 """
1222 message = MIMEMultipart('mixed')
1223
1224 setattr(message, '_write_headers', lambda self: None)
1225
1226
1227 for request_id in order:
1228 request = requests[request_id]
1229
1230 msg = MIMENonMultipart('application', 'http')
1231 msg['Content-Transfer-Encoding'] = 'binary'
1232 msg['Content-ID'] = self._id_to_header(request_id)
1233
1234 body = self._serialize_request(request)
1235 msg.set_payload(body)
1236 message.attach(msg)
1237
1238
1239
1240 fp = StringIO()
1241 g = Generator(fp, mangle_from_=False)
1242 g.flatten(message, unixfrom=False)
1243 body = fp.getvalue()
1244
1245 headers = {}
1246 headers['content-type'] = ('multipart/mixed; '
1247 'boundary="%s"') % message.get_boundary()
1248
1249 resp, content = http.request(self._batch_uri, method='POST', body=body,
1250 headers=headers)
1251
1252 if resp.status >= 300:
1253 raise HttpError(resp, content, uri=self._batch_uri)
1254
1255
1256 boundary, _ = content.split(None, 1)
1257
1258
1259 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1260 for_parser = header + content
1261
1262 parser = FeedParser()
1263 parser.feed(for_parser)
1264 mime_response = parser.close()
1265
1266 if not mime_response.is_multipart():
1267 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1268 content=content)
1269
1270 for part in mime_response.get_payload():
1271 request_id = self._header_to_id(part['Content-ID'])
1272 response, content = self._deserialize_response(part.get_payload())
1273 self._responses[request_id] = (response, content)
1274
1275 @util.positional(1)
1277 """Execute all the requests as a single batched HTTP request.
1278
1279 Args:
1280 http: httplib2.Http, an http object to be used in place of the one the
1281 HttpRequest request object was constructed with. If one isn't supplied
1282 then use a http object from the requests in this batch.
1283
1284 Returns:
1285 None
1286
1287 Raises:
1288 httplib2.HttpLib2Error if a transport error has occured.
1289 googleapiclient.errors.BatchError if the response is the wrong format.
1290 """
1291
1292
1293 if http is None:
1294 for request_id in self._order:
1295 request = self._requests[request_id]
1296 if request is not None:
1297 http = request.http
1298 break
1299
1300 if http is None:
1301 raise ValueError("Missing a valid http object.")
1302
1303 self._execute(http, self._order, self._requests)
1304
1305
1306
1307 redo_requests = {}
1308 redo_order = []
1309
1310 for request_id in self._order:
1311 resp, content = self._responses[request_id]
1312 if resp['status'] == '401':
1313 redo_order.append(request_id)
1314 request = self._requests[request_id]
1315 self._refresh_and_apply_credentials(request, http)
1316 redo_requests[request_id] = request
1317
1318 if redo_requests:
1319 self._execute(http, redo_order, redo_requests)
1320
1321
1322
1323
1324
1325 for request_id in self._order:
1326 resp, content = self._responses[request_id]
1327
1328 request = self._requests[request_id]
1329 callback = self._callbacks[request_id]
1330
1331 response = None
1332 exception = None
1333 try:
1334 if resp.status >= 300:
1335 raise HttpError(resp, content, uri=request.uri)
1336 response = request.postproc(resp, content)
1337 except HttpError as e:
1338 exception = e
1339
1340 if callback is not None:
1341 callback(request_id, response, exception)
1342 if self._callback is not None:
1343 self._callback(request_id, response, exception)
1344
1347 """Mock of HttpRequest.
1348
1349 Do not construct directly, instead use RequestMockBuilder.
1350 """
1351
1352 - def __init__(self, resp, content, postproc):
1353 """Constructor for HttpRequestMock
1354
1355 Args:
1356 resp: httplib2.Response, the response to emulate coming from the request
1357 content: string, the response body
1358 postproc: callable, the post processing function usually supplied by
1359 the model class. See model.JsonModel.response() as an example.
1360 """
1361 self.resp = resp
1362 self.content = content
1363 self.postproc = postproc
1364 if resp is None:
1365 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1366 if 'reason' in self.resp:
1367 self.resp.reason = self.resp['reason']
1368
1370 """Execute the request.
1371
1372 Same behavior as HttpRequest.execute(), but the response is
1373 mocked and not really from an HTTP request/response.
1374 """
1375 return self.postproc(self.resp, self.content)
1376
1379 """A simple mock of HttpRequest
1380
1381 Pass in a dictionary to the constructor that maps request methodIds to
1382 tuples of (httplib2.Response, content, opt_expected_body) that should be
1383 returned when that method is called. None may also be passed in for the
1384 httplib2.Response, in which case a 200 OK response will be generated.
1385 If an opt_expected_body (str or dict) is provided, it will be compared to
1386 the body and UnexpectedBodyError will be raised on inequality.
1387
1388 Example:
1389 response = '{"data": {"id": "tag:google.c...'
1390 requestBuilder = RequestMockBuilder(
1391 {
1392 'plus.activities.get': (None, response),
1393 }
1394 )
1395 googleapiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1396
1397 Methods that you do not supply a response for will return a
1398 200 OK with an empty string as the response content or raise an excpetion
1399 if check_unexpected is set to True. The methodId is taken from the rpcName
1400 in the discovery document.
1401
1402 For more details see the project wiki.
1403 """
1404
1405 - def __init__(self, responses, check_unexpected=False):
1406 """Constructor for RequestMockBuilder
1407
1408 The constructed object should be a callable object
1409 that can replace the class HttpResponse.
1410
1411 responses - A dictionary that maps methodIds into tuples
1412 of (httplib2.Response, content). The methodId
1413 comes from the 'rpcName' field in the discovery
1414 document.
1415 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1416 should be raised on unsupplied method.
1417 """
1418 self.responses = responses
1419 self.check_unexpected = check_unexpected
1420
1421 - def __call__(self, http, postproc, uri, method='GET', body=None,
1422 headers=None, methodId=None, resumable=None):
1423 """Implements the callable interface that discovery.build() expects
1424 of requestBuilder, which is to build an object compatible with
1425 HttpRequest.execute(). See that method for the description of the
1426 parameters and the expected response.
1427 """
1428 if methodId in self.responses:
1429 response = self.responses[methodId]
1430 resp, content = response[:2]
1431 if len(response) > 2:
1432
1433 expected_body = response[2]
1434 if bool(expected_body) != bool(body):
1435
1436
1437 raise UnexpectedBodyError(expected_body, body)
1438 if isinstance(expected_body, str):
1439 expected_body = json.loads(expected_body)
1440 body = json.loads(body)
1441 if body != expected_body:
1442 raise UnexpectedBodyError(expected_body, body)
1443 return HttpRequestMock(resp, content, postproc)
1444 elif self.check_unexpected:
1445 raise UnexpectedMethodError(methodId=methodId)
1446 else:
1447 model = JsonModel(False)
1448 return HttpRequestMock(None, '{}', model.response)
1449
1452 """Mock of httplib2.Http"""
1453
1454 - def __init__(self, filename=None, headers=None):
1455 """
1456 Args:
1457 filename: string, absolute filename to read response from
1458 headers: dict, header to return with response
1459 """
1460 if headers is None:
1461 headers = {'status': '200 OK'}
1462 if filename:
1463 f = open(filename, 'r')
1464 self.data = f.read()
1465 f.close()
1466 else:
1467 self.data = None
1468 self.response_headers = headers
1469 self.headers = None
1470 self.uri = None
1471 self.method = None
1472 self.body = None
1473 self.headers = None
1474
1475
1476 - def request(self, uri,
1477 method='GET',
1478 body=None,
1479 headers=None,
1480 redirections=1,
1481 connection_type=None):
1482 self.uri = uri
1483 self.method = method
1484 self.body = body
1485 self.headers = headers
1486 return httplib2.Response(self.response_headers), self.data
1487
1490 """Mock of httplib2.Http
1491
1492 Mocks a sequence of calls to request returning different responses for each
1493 call. Create an instance initialized with the desired response headers
1494 and content and then use as if an httplib2.Http instance.
1495
1496 http = HttpMockSequence([
1497 ({'status': '401'}, ''),
1498 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1499 ({'status': '200'}, 'echo_request_headers'),
1500 ])
1501 resp, content = http.request("http://examples.com")
1502
1503 There are special values you can pass in for content to trigger
1504 behavours that are helpful in testing.
1505
1506 'echo_request_headers' means return the request headers in the response body
1507 'echo_request_headers_as_json' means return the request headers in
1508 the response body
1509 'echo_request_body' means return the request body in the response body
1510 'echo_request_uri' means return the request uri in the response body
1511 """
1512
1514 """
1515 Args:
1516 iterable: iterable, a sequence of pairs of (headers, body)
1517 """
1518 self._iterable = iterable
1519 self.follow_redirects = True
1520
1521 - def request(self, uri,
1522 method='GET',
1523 body=None,
1524 headers=None,
1525 redirections=1,
1526 connection_type=None):
1527 resp, content = self._iterable.pop(0)
1528 if content == 'echo_request_headers':
1529 content = headers
1530 elif content == 'echo_request_headers_as_json':
1531 content = json.dumps(headers)
1532 elif content == 'echo_request_body':
1533 if hasattr(body, 'read'):
1534 content = body.read()
1535 else:
1536 content = body
1537 elif content == 'echo_request_uri':
1538 content = uri
1539 return httplib2.Response(resp), content
1540
1543 """Set the user-agent on every request.
1544
1545 Args:
1546 http - An instance of httplib2.Http
1547 or something that acts like it.
1548 user_agent: string, the value for the user-agent header.
1549
1550 Returns:
1551 A modified instance of http that was passed in.
1552
1553 Example:
1554
1555 h = httplib2.Http()
1556 h = set_user_agent(h, "my-app-name/6.0")
1557
1558 Most of the time the user-agent will be set doing auth, this is for the rare
1559 cases where you are accessing an unauthenticated endpoint.
1560 """
1561 request_orig = http.request
1562
1563
1564 def new_request(uri, method='GET', body=None, headers=None,
1565 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1566 connection_type=None):
1567 """Modify the request headers to add the user-agent."""
1568 if headers is None:
1569 headers = {}
1570 if 'user-agent' in headers:
1571 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1572 else:
1573 headers['user-agent'] = user_agent
1574 resp, content = request_orig(uri, method, body, headers,
1575 redirections, connection_type)
1576 return resp, content
1577
1578 http.request = new_request
1579 return http
1580
1583 """Tunnel PATCH requests over POST.
1584 Args:
1585 http - An instance of httplib2.Http
1586 or something that acts like it.
1587
1588 Returns:
1589 A modified instance of http that was passed in.
1590
1591 Example:
1592
1593 h = httplib2.Http()
1594 h = tunnel_patch(h, "my-app-name/6.0")
1595
1596 Useful if you are running on a platform that doesn't support PATCH.
1597 Apply this last if you are using OAuth 1.0, as changing the method
1598 will result in a different signature.
1599 """
1600 request_orig = http.request
1601
1602
1603 def new_request(uri, method='GET', body=None, headers=None,
1604 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1605 connection_type=None):
1606 """Modify the request headers to add the user-agent."""
1607 if headers is None:
1608 headers = {}
1609 if method == 'PATCH':
1610 if 'oauth_token' in headers.get('authorization', ''):
1611 logging.warning(
1612 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1613 headers['x-http-method-override'] = "PATCH"
1614 method = 'POST'
1615 resp, content = request_orig(uri, method, body, headers,
1616 redirections, connection_type)
1617 return resp, content
1618
1619 http.request = new_request
1620 return http
1621