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