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 return status_line + body
1124
1126 """Convert string into httplib2 response and content.
1127
1128 Args:
1129 payload: string, headers and body as a string.
1130
1131 Returns:
1132 A pair (resp, content), such as would be returned from httplib2.request.
1133 """
1134
1135 status_line, payload = payload.split('\n', 1)
1136 protocol, status, reason = status_line.split(' ', 2)
1137
1138
1139 parser = FeedParser()
1140 parser.feed(payload)
1141 msg = parser.close()
1142 msg['status'] = status
1143
1144
1145 resp = httplib2.Response(msg)
1146 resp.reason = reason
1147 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
1148
1149 content = payload.split('\r\n\r\n', 1)[1]
1150
1151 return resp, content
1152
1154 """Create a new id.
1155
1156 Auto incrementing number that avoids conflicts with ids already used.
1157
1158 Returns:
1159 string, a new unique id.
1160 """
1161 self._last_auto_id += 1
1162 while str(self._last_auto_id) in self._requests:
1163 self._last_auto_id += 1
1164 return str(self._last_auto_id)
1165
1166 @util.positional(2)
1167 - def add(self, request, callback=None, request_id=None):
1168 """Add a new request.
1169
1170 Every callback added will be paired with a unique id, the request_id. That
1171 unique id will be passed back to the callback when the response comes back
1172 from the server. The default behavior is to have the library generate it's
1173 own unique id. If the caller passes in a request_id then they must ensure
1174 uniqueness for each request_id, and if they are not an exception is
1175 raised. Callers should either supply all request_ids or nevery supply a
1176 request id, to avoid such an error.
1177
1178 Args:
1179 request: HttpRequest, Request to add to the batch.
1180 callback: callable, A callback to be called for this response, of the
1181 form callback(id, response, exception). The first parameter is the
1182 request id, and the second is the deserialized response object. The
1183 third is an googleapiclient.errors.HttpError exception object if an HTTP error
1184 occurred while processing the request, or None if no errors occurred.
1185 request_id: string, A unique id for the request. The id will be passed to
1186 the callback with the response.
1187
1188 Returns:
1189 None
1190
1191 Raises:
1192 BatchError if a media request is added to a batch.
1193 KeyError is the request_id is not unique.
1194 """
1195 if request_id is None:
1196 request_id = self._new_id()
1197 if request.resumable is not None:
1198 raise BatchError("Media requests cannot be used in a batch request.")
1199 if request_id in self._requests:
1200 raise KeyError("A request with this ID already exists: %s" % request_id)
1201 self._requests[request_id] = request
1202 self._callbacks[request_id] = callback
1203 self._order.append(request_id)
1204
1205 - def _execute(self, http, order, requests):
1206 """Serialize batch request, send to server, process response.
1207
1208 Args:
1209 http: httplib2.Http, an http object to be used to make the request with.
1210 order: list, list of request ids in the order they were added to the
1211 batch.
1212 request: list, list of request objects to send.
1213
1214 Raises:
1215 httplib2.HttpLib2Error if a transport error has occured.
1216 googleapiclient.errors.BatchError if the response is the wrong format.
1217 """
1218 message = MIMEMultipart('mixed')
1219
1220 setattr(message, '_write_headers', lambda self: None)
1221
1222
1223 for request_id in order:
1224 request = requests[request_id]
1225
1226 msg = MIMENonMultipart('application', 'http')
1227 msg['Content-Transfer-Encoding'] = 'binary'
1228 msg['Content-ID'] = self._id_to_header(request_id)
1229
1230 body = self._serialize_request(request)
1231 msg.set_payload(body)
1232 message.attach(msg)
1233
1234
1235
1236 fp = StringIO()
1237 g = Generator(fp, mangle_from_=False)
1238 g.flatten(message, unixfrom=False)
1239 body = fp.getvalue()
1240
1241 headers = {}
1242 headers['content-type'] = ('multipart/mixed; '
1243 'boundary="%s"') % message.get_boundary()
1244
1245 resp, content = http.request(self._batch_uri, method='POST', body=body,
1246 headers=headers)
1247
1248 if resp.status >= 300:
1249 raise HttpError(resp, content, uri=self._batch_uri)
1250
1251
1252 header = 'content-type: %s\r\n\r\n' % resp['content-type']
1253
1254
1255 if six.PY3:
1256 content = content.decode('utf-8')
1257 for_parser = header + content
1258
1259 parser = FeedParser()
1260 parser.feed(for_parser)
1261 mime_response = parser.close()
1262
1263 if not mime_response.is_multipart():
1264 raise BatchError("Response not in multipart/mixed format.", resp=resp,
1265 content=content)
1266
1267 for part in mime_response.get_payload():
1268 request_id = self._header_to_id(part['Content-ID'])
1269 response, content = self._deserialize_response(part.get_payload())
1270
1271 if isinstance(content, six.text_type):
1272 content = content.encode('utf-8')
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'}
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 if isinstance(content, six.text_type):
1540 content = content.encode('utf-8')
1541 return httplib2.Response(resp), content
1542
1545 """Set the user-agent on every request.
1546
1547 Args:
1548 http - An instance of httplib2.Http
1549 or something that acts like it.
1550 user_agent: string, the value for the user-agent header.
1551
1552 Returns:
1553 A modified instance of http that was passed in.
1554
1555 Example:
1556
1557 h = httplib2.Http()
1558 h = set_user_agent(h, "my-app-name/6.0")
1559
1560 Most of the time the user-agent will be set doing auth, this is for the rare
1561 cases where you are accessing an unauthenticated endpoint.
1562 """
1563 request_orig = http.request
1564
1565
1566 def new_request(uri, method='GET', body=None, headers=None,
1567 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1568 connection_type=None):
1569 """Modify the request headers to add the user-agent."""
1570 if headers is None:
1571 headers = {}
1572 if 'user-agent' in headers:
1573 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1574 else:
1575 headers['user-agent'] = user_agent
1576 resp, content = request_orig(uri, method, body, headers,
1577 redirections, connection_type)
1578 return resp, content
1579
1580 http.request = new_request
1581 return http
1582
1585 """Tunnel PATCH requests over POST.
1586 Args:
1587 http - An instance of httplib2.Http
1588 or something that acts like it.
1589
1590 Returns:
1591 A modified instance of http that was passed in.
1592
1593 Example:
1594
1595 h = httplib2.Http()
1596 h = tunnel_patch(h, "my-app-name/6.0")
1597
1598 Useful if you are running on a platform that doesn't support PATCH.
1599 Apply this last if you are using OAuth 1.0, as changing the method
1600 will result in a different signature.
1601 """
1602 request_orig = http.request
1603
1604
1605 def new_request(uri, method='GET', body=None, headers=None,
1606 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1607 connection_type=None):
1608 """Modify the request headers to add the user-agent."""
1609 if headers is None:
1610 headers = {}
1611 if method == 'PATCH':
1612 if 'oauth_token' in headers.get('authorization', ''):
1613 logging.warning(
1614 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1615 headers['x-http-method-override'] = "PATCH"
1616 method = 'POST'
1617 resp, content = request_orig(uri, method, body, headers,
1618 redirections, connection_type)
1619 return resp, content
1620
1621 http.request = new_request
1622 return http
1623