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