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