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 mimeparse
30 import mimetypes
31 import os
32 import urllib
33 import urlparse
34 import uuid
35
36 from email.generator import Generator
37 from email.mime.multipart import MIMEMultipart
38 from email.mime.nonmultipart import MIMENonMultipart
39 from email.parser import FeedParser
40 from errors import BatchError
41 from errors import HttpError
42 from errors import ResumableUploadError
43 from errors import UnexpectedBodyError
44 from errors import UnexpectedMethodError
45 from model import JsonModel
46 from oauth2client.anyjson import simplejson
47
48
49 DEFAULT_CHUNK_SIZE = 512*1024
77
184
284
380
473
476 """Encapsulates a single HTTP request."""
477
478 - def __init__(self, http, postproc, uri,
479 method='GET',
480 body=None,
481 headers=None,
482 methodId=None,
483 resumable=None):
484 """Constructor for an HttpRequest.
485
486 Args:
487 http: httplib2.Http, the transport object to use to make a request
488 postproc: callable, called on the HTTP response and content to transform
489 it into a data object before returning, or raising an exception
490 on an error.
491 uri: string, the absolute URI to send the request to
492 method: string, the HTTP method to use
493 body: string, the request body of the HTTP request,
494 headers: dict, the HTTP request headers
495 methodId: string, a unique identifier for the API method being called.
496 resumable: MediaUpload, None if this is not a resumbale request.
497 """
498 self.uri = uri
499 self.method = method
500 self.body = body
501 self.headers = headers or {}
502 self.methodId = methodId
503 self.http = http
504 self.postproc = postproc
505 self.resumable = resumable
506 self._in_error_state = False
507
508
509 major, minor, params = mimeparse.parse_mime_type(
510 headers.get('content-type', 'application/json'))
511
512
513 self.body_size = len(self.body or '')
514
515
516 self.resumable_uri = None
517
518
519 self.resumable_progress = 0
520
522 """Execute the request.
523
524 Args:
525 http: httplib2.Http, an http object to be used in place of the
526 one the HttpRequest request object was constructed with.
527
528 Returns:
529 A deserialized object model of the response body as determined
530 by the postproc.
531
532 Raises:
533 apiclient.errors.HttpError if the response was not a 2xx.
534 httplib2.Error if a transport error has occured.
535 """
536 if http is None:
537 http = self.http
538 if self.resumable:
539 body = None
540 while body is None:
541 _, body = self.next_chunk(http)
542 return body
543 else:
544 if 'content-length' not in self.headers:
545 self.headers['content-length'] = str(self.body_size)
546 resp, content = http.request(self.uri, self.method,
547 body=self.body,
548 headers=self.headers)
549
550 if resp.status >= 300:
551 raise HttpError(resp, content, self.uri)
552 return self.postproc(resp, content)
553
555 """Execute the next step of a resumable upload.
556
557 Can only be used if the method being executed supports media uploads and
558 the MediaUpload object passed in was flagged as using resumable upload.
559
560 Example:
561
562 media = MediaFileUpload('smiley.png', mimetype='image/png',
563 chunksize=1000, resumable=True)
564 request = service.objects().insert(
565 bucket=buckets['items'][0]['id'],
566 name='smiley.png',
567 media_body=media)
568
569 response = None
570 while response is None:
571 status, response = request.next_chunk()
572 if status:
573 print "Upload %d%% complete." % int(status.progress() * 100)
574
575
576 Returns:
577 (status, body): (ResumableMediaStatus, object)
578 The body will be None until the resumable media is fully uploaded.
579
580 Raises:
581 apiclient.errors.HttpError if the response was not a 2xx.
582 httplib2.Error if a transport error has occured.
583 """
584 if http is None:
585 http = self.http
586
587 if self.resumable.size() is None:
588 size = '*'
589 else:
590 size = str(self.resumable.size())
591
592 if self.resumable_uri is None:
593 start_headers = copy.copy(self.headers)
594 start_headers['X-Upload-Content-Type'] = self.resumable.mimetype()
595 if size != '*':
596 start_headers['X-Upload-Content-Length'] = size
597 start_headers['content-length'] = str(self.body_size)
598
599 resp, content = http.request(self.uri, self.method,
600 body=self.body,
601 headers=start_headers)
602 if resp.status == 200 and 'location' in resp:
603 self.resumable_uri = resp['location']
604 else:
605 raise ResumableUploadError("Failed to retrieve starting URI.")
606 elif self._in_error_state:
607
608
609
610 headers = {
611 'Content-Range': 'bytes */%s' % size,
612 'content-length': '0'
613 }
614 resp, content = http.request(self.resumable_uri, 'PUT',
615 headers=headers)
616 status, body = self._process_response(resp, content)
617 if body:
618
619 return (status, body)
620
621 data = self.resumable.getbytes(
622 self.resumable_progress, self.resumable.chunksize())
623
624
625 if len(data) < self.resumable.chunksize():
626 size = str(self.resumable_progress + len(data))
627
628 headers = {
629 'Content-Range': 'bytes %d-%d/%s' % (
630 self.resumable_progress, self.resumable_progress + len(data) - 1,
631 size)
632 }
633 try:
634 resp, content = http.request(self.resumable_uri, 'PUT',
635 body=data,
636 headers=headers)
637 except:
638 self._in_error_state = True
639 raise
640
641 return self._process_response(resp, content)
642
644 """Process the response from a single chunk upload.
645
646 Args:
647 resp: httplib2.Response, the response object.
648 content: string, the content of the response.
649
650 Returns:
651 (status, body): (ResumableMediaStatus, object)
652 The body will be None until the resumable media is fully uploaded.
653
654 Raises:
655 apiclient.errors.HttpError if the response was not a 2xx or a 308.
656 """
657 if resp.status in [200, 201]:
658 self._in_error_state = False
659 return None, self.postproc(resp, content)
660 elif resp.status == 308:
661 self._in_error_state = False
662
663 self.resumable_progress = int(resp['range'].split('-')[1]) + 1
664 if 'location' in resp:
665 self.resumable_uri = resp['location']
666 else:
667 self._in_error_state = True
668 raise HttpError(resp, content, self.uri)
669
670 return (MediaUploadProgress(self.resumable_progress, self.resumable.size()),
671 None)
672
674 """Returns a JSON representation of the HttpRequest."""
675 d = copy.copy(self.__dict__)
676 if d['resumable'] is not None:
677 d['resumable'] = self.resumable.to_json()
678 del d['http']
679 del d['postproc']
680
681 return simplejson.dumps(d)
682
683 @staticmethod
685 """Returns an HttpRequest populated with info from a JSON object."""
686 d = simplejson.loads(s)
687 if d['resumable'] is not None:
688 d['resumable'] = MediaUpload.new_from_json(d['resumable'])
689 return HttpRequest(
690 http,
691 postproc,
692 uri=d['uri'],
693 method=d['method'],
694 body=d['body'],
695 headers=d['headers'],
696 methodId=d['methodId'],
697 resumable=d['resumable'])
698
701 """Batches multiple HttpRequest objects into a single HTTP request."""
702
703 - def __init__(self, callback=None, batch_uri=None):
704 """Constructor for a BatchHttpRequest.
705
706 Args:
707 callback: callable, A callback to be called for each response, of the
708 form callback(id, response). The first parameter is the request id, and
709 the second is the deserialized response object.
710 batch_uri: string, URI to send batch requests to.
711 """
712 if batch_uri is None:
713 batch_uri = 'https://www.googleapis.com/batch'
714 self._batch_uri = batch_uri
715
716
717 self._callback = callback
718
719
720 self._requests = {}
721
722
723 self._callbacks = {}
724
725
726 self._order = []
727
728
729 self._last_auto_id = 0
730
731
732 self._base_id = None
733
734
735 self._responses = {}
736
737
738 self._refreshed_credentials = {}
739
766
768 """Convert an id to a Content-ID header value.
769
770 Args:
771 id_: string, identifier of individual request.
772
773 Returns:
774 A Content-ID header with the id_ encoded into it. A UUID is prepended to
775 the value because Content-ID headers are supposed to be universally
776 unique.
777 """
778 if self._base_id is None:
779 self._base_id = uuid.uuid4()
780
781 return '<%s+%s>' % (self._base_id, urllib.quote(id_))
782
784 """Convert a Content-ID header value to an id.
785
786 Presumes the Content-ID header conforms to the format that _id_to_header()
787 returns.
788
789 Args:
790 header: string, Content-ID header value.
791
792 Returns:
793 The extracted id value.
794
795 Raises:
796 BatchError if the header is not in the expected format.
797 """
798 if header[0] != '<' or header[-1] != '>':
799 raise BatchError("Invalid value for Content-ID: %s" % header)
800 if '+' not in header:
801 raise BatchError("Invalid value for Content-ID: %s" % header)
802 base, id_ = header[1:-1].rsplit('+', 1)
803
804 return urllib.unquote(id_)
805
807 """Convert an HttpRequest object into a string.
808
809 Args:
810 request: HttpRequest, the request to serialize.
811
812 Returns:
813 The request as a string in application/http format.
814 """
815
816 parsed = urlparse.urlparse(request.uri)
817 request_line = urlparse.urlunparse(
818 (None, None, parsed.path, parsed.params, parsed.query, None)
819 )
820 status_line = request.method + ' ' + request_line + ' HTTP/1.1\n'
821 major, minor = request.headers.get('content-type', 'application/json').split('/')
822 msg = MIMENonMultipart(major, minor)
823 headers = request.headers.copy()
824
825 if request.http is not None and hasattr(request.http.request,
826 'credentials'):
827 request.http.request.credentials.apply(headers)
828
829
830 if 'content-type' in headers:
831 del headers['content-type']
832
833 for key, value in headers.iteritems():
834 msg[key] = value
835 msg['Host'] = parsed.netloc
836 msg.set_unixfrom(None)
837
838 if request.body is not None:
839 msg.set_payload(request.body)
840 msg['content-length'] = str(len(request.body))
841
842
843 fp = StringIO.StringIO()
844
845 g = Generator(fp, maxheaderlen=0)
846 g.flatten(msg, unixfrom=False)
847 body = fp.getvalue()
848
849
850 if request.body is None:
851 body = body[:-2]
852
853 return status_line.encode('utf-8') + body
854
856 """Convert string into httplib2 response and content.
857
858 Args:
859 payload: string, headers and body as a string.
860
861 Returns:
862 A pair (resp, content) like would be returned from httplib2.request.
863 """
864
865 status_line, payload = payload.split('\n', 1)
866 protocol, status, reason = status_line.split(' ', 2)
867
868
869 parser = FeedParser()
870 parser.feed(payload)
871 msg = parser.close()
872 msg['status'] = status
873
874
875 resp = httplib2.Response(msg)
876 resp.reason = reason
877 resp.version = int(protocol.split('/', 1)[1].replace('.', ''))
878
879 content = payload.split('\r\n\r\n', 1)[1]
880
881 return resp, content
882
884 """Create a new id.
885
886 Auto incrementing number that avoids conflicts with ids already used.
887
888 Returns:
889 string, a new unique id.
890 """
891 self._last_auto_id += 1
892 while str(self._last_auto_id) in self._requests:
893 self._last_auto_id += 1
894 return str(self._last_auto_id)
895
896 - def add(self, request, callback=None, request_id=None):
897 """Add a new request.
898
899 Every callback added will be paired with a unique id, the request_id. That
900 unique id will be passed back to the callback when the response comes back
901 from the server. The default behavior is to have the library generate it's
902 own unique id. If the caller passes in a request_id then they must ensure
903 uniqueness for each request_id, and if they are not an exception is
904 raised. Callers should either supply all request_ids or nevery supply a
905 request id, to avoid such an error.
906
907 Args:
908 request: HttpRequest, Request to add to the batch.
909 callback: callable, A callback to be called for this response, of the
910 form callback(id, response). The first parameter is the request id, and
911 the second is the deserialized response object.
912 request_id: string, A unique id for the request. The id will be passed to
913 the callback with the response.
914
915 Returns:
916 None
917
918 Raises:
919 BatchError if a resumable request is added to a batch.
920 KeyError is the request_id is not unique.
921 """
922 if request_id is None:
923 request_id = self._new_id()
924 if request.resumable is not None:
925 raise BatchError("Resumable requests cannot be used in a batch request.")
926 if request_id in self._requests:
927 raise KeyError("A request with this ID already exists: %s" % request_id)
928 self._requests[request_id] = request
929 self._callbacks[request_id] = callback
930 self._order.append(request_id)
931
932 - def _execute(self, http, order, requests):
933 """Serialize batch request, send to server, process response.
934
935 Args:
936 http: httplib2.Http, an http object to be used to make the request with.
937 order: list, list of request ids in the order they were added to the
938 batch.
939 request: list, list of request objects to send.
940
941 Raises:
942 httplib2.Error if a transport error has occured.
943 apiclient.errors.BatchError if the response is the wrong format.
944 """
945 message = MIMEMultipart('mixed')
946
947 setattr(message, '_write_headers', lambda self: None)
948
949
950 for request_id in order:
951 request = requests[request_id]
952
953 msg = MIMENonMultipart('application', 'http')
954 msg['Content-Transfer-Encoding'] = 'binary'
955 msg['Content-ID'] = self._id_to_header(request_id)
956
957 body = self._serialize_request(request)
958 msg.set_payload(body)
959 message.attach(msg)
960
961 body = message.as_string()
962
963 headers = {}
964 headers['content-type'] = ('multipart/mixed; '
965 'boundary="%s"') % message.get_boundary()
966
967 resp, content = http.request(self._batch_uri, 'POST', body=body,
968 headers=headers)
969
970 if resp.status >= 300:
971 raise HttpError(resp, content, self._batch_uri)
972
973
974 boundary, _ = content.split(None, 1)
975
976
977 header = 'content-type: %s\r\n\r\n' % resp['content-type']
978 for_parser = header + content
979
980 parser = FeedParser()
981 parser.feed(for_parser)
982 mime_response = parser.close()
983
984 if not mime_response.is_multipart():
985 raise BatchError("Response not in multipart/mixed format.", resp,
986 content)
987
988 for part in mime_response.get_payload():
989 request_id = self._header_to_id(part['Content-ID'])
990 headers, content = self._deserialize_response(part.get_payload())
991 self._responses[request_id] = (headers, content)
992
994 """Execute all the requests as a single batched HTTP request.
995
996 Args:
997 http: httplib2.Http, an http object to be used in place of the one the
998 HttpRequest request object was constructed with. If one isn't supplied
999 then use a http object from the requests in this batch.
1000
1001 Returns:
1002 None
1003
1004 Raises:
1005 httplib2.Error if a transport error has occured.
1006 apiclient.errors.BatchError if the response is the wrong format.
1007 """
1008
1009
1010 if http is None:
1011 for request_id in self._order:
1012 request = self._requests[request_id]
1013 if request is not None:
1014 http = request.http
1015 break
1016
1017 if http is None:
1018 raise ValueError("Missing a valid http object.")
1019
1020 self._execute(http, self._order, self._requests)
1021
1022
1023
1024 redo_requests = {}
1025 redo_order = []
1026
1027 for request_id in self._order:
1028 headers, content = self._responses[request_id]
1029 if headers['status'] == '401':
1030 redo_order.append(request_id)
1031 request = self._requests[request_id]
1032 self._refresh_and_apply_credentials(request, http)
1033 redo_requests[request_id] = request
1034
1035 if redo_requests:
1036 self._execute(http, redo_order, redo_requests)
1037
1038
1039
1040
1041
1042 for request_id in self._order:
1043 headers, content = self._responses[request_id]
1044
1045 request = self._requests[request_id]
1046 callback = self._callbacks[request_id]
1047
1048 response = None
1049 exception = None
1050 try:
1051 r = httplib2.Response(headers)
1052 response = request.postproc(r, content)
1053 except HttpError, e:
1054 exception = e
1055
1056 if callback is not None:
1057 callback(request_id, response, exception)
1058 if self._callback is not None:
1059 self._callback(request_id, response, exception)
1060
1063 """Mock of HttpRequest.
1064
1065 Do not construct directly, instead use RequestMockBuilder.
1066 """
1067
1068 - def __init__(self, resp, content, postproc):
1069 """Constructor for HttpRequestMock
1070
1071 Args:
1072 resp: httplib2.Response, the response to emulate coming from the request
1073 content: string, the response body
1074 postproc: callable, the post processing function usually supplied by
1075 the model class. See model.JsonModel.response() as an example.
1076 """
1077 self.resp = resp
1078 self.content = content
1079 self.postproc = postproc
1080 if resp is None:
1081 self.resp = httplib2.Response({'status': 200, 'reason': 'OK'})
1082 if 'reason' in self.resp:
1083 self.resp.reason = self.resp['reason']
1084
1086 """Execute the request.
1087
1088 Same behavior as HttpRequest.execute(), but the response is
1089 mocked and not really from an HTTP request/response.
1090 """
1091 return self.postproc(self.resp, self.content)
1092
1095 """A simple mock of HttpRequest
1096
1097 Pass in a dictionary to the constructor that maps request methodIds to
1098 tuples of (httplib2.Response, content, opt_expected_body) that should be
1099 returned when that method is called. None may also be passed in for the
1100 httplib2.Response, in which case a 200 OK response will be generated.
1101 If an opt_expected_body (str or dict) is provided, it will be compared to
1102 the body and UnexpectedBodyError will be raised on inequality.
1103
1104 Example:
1105 response = '{"data": {"id": "tag:google.c...'
1106 requestBuilder = RequestMockBuilder(
1107 {
1108 'plus.activities.get': (None, response),
1109 }
1110 )
1111 apiclient.discovery.build("plus", "v1", requestBuilder=requestBuilder)
1112
1113 Methods that you do not supply a response for will return a
1114 200 OK with an empty string as the response content or raise an excpetion
1115 if check_unexpected is set to True. The methodId is taken from the rpcName
1116 in the discovery document.
1117
1118 For more details see the project wiki.
1119 """
1120
1121 - def __init__(self, responses, check_unexpected=False):
1122 """Constructor for RequestMockBuilder
1123
1124 The constructed object should be a callable object
1125 that can replace the class HttpResponse.
1126
1127 responses - A dictionary that maps methodIds into tuples
1128 of (httplib2.Response, content). The methodId
1129 comes from the 'rpcName' field in the discovery
1130 document.
1131 check_unexpected - A boolean setting whether or not UnexpectedMethodError
1132 should be raised on unsupplied method.
1133 """
1134 self.responses = responses
1135 self.check_unexpected = check_unexpected
1136
1137 - def __call__(self, http, postproc, uri, method='GET', body=None,
1138 headers=None, methodId=None, resumable=None):
1139 """Implements the callable interface that discovery.build() expects
1140 of requestBuilder, which is to build an object compatible with
1141 HttpRequest.execute(). See that method for the description of the
1142 parameters and the expected response.
1143 """
1144 if methodId in self.responses:
1145 response = self.responses[methodId]
1146 resp, content = response[:2]
1147 if len(response) > 2:
1148
1149 expected_body = response[2]
1150 if bool(expected_body) != bool(body):
1151
1152
1153 raise UnexpectedBodyError(expected_body, body)
1154 if isinstance(expected_body, str):
1155 expected_body = simplejson.loads(expected_body)
1156 body = simplejson.loads(body)
1157 if body != expected_body:
1158 raise UnexpectedBodyError(expected_body, body)
1159 return HttpRequestMock(resp, content, postproc)
1160 elif self.check_unexpected:
1161 raise UnexpectedMethodError(methodId)
1162 else:
1163 model = JsonModel(False)
1164 return HttpRequestMock(None, '{}', model.response)
1165
1168 """Mock of httplib2.Http"""
1169
1170 - def __init__(self, filename, headers=None):
1171 """
1172 Args:
1173 filename: string, absolute filename to read response from
1174 headers: dict, header to return with response
1175 """
1176 if headers is None:
1177 headers = {'status': '200 OK'}
1178 f = file(filename, 'r')
1179 self.data = f.read()
1180 f.close()
1181 self.headers = headers
1182
1183 - def request(self, uri,
1184 method='GET',
1185 body=None,
1186 headers=None,
1187 redirections=1,
1188 connection_type=None):
1189 return httplib2.Response(self.headers), self.data
1190
1193 """Mock of httplib2.Http
1194
1195 Mocks a sequence of calls to request returning different responses for each
1196 call. Create an instance initialized with the desired response headers
1197 and content and then use as if an httplib2.Http instance.
1198
1199 http = HttpMockSequence([
1200 ({'status': '401'}, ''),
1201 ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'),
1202 ({'status': '200'}, 'echo_request_headers'),
1203 ])
1204 resp, content = http.request("http://examples.com")
1205
1206 There are special values you can pass in for content to trigger
1207 behavours that are helpful in testing.
1208
1209 'echo_request_headers' means return the request headers in the response body
1210 'echo_request_headers_as_json' means return the request headers in
1211 the response body
1212 'echo_request_body' means return the request body in the response body
1213 'echo_request_uri' means return the request uri in the response body
1214 """
1215
1217 """
1218 Args:
1219 iterable: iterable, a sequence of pairs of (headers, body)
1220 """
1221 self._iterable = iterable
1222
1223 - def request(self, uri,
1224 method='GET',
1225 body=None,
1226 headers=None,
1227 redirections=1,
1228 connection_type=None):
1229 resp, content = self._iterable.pop(0)
1230 if content == 'echo_request_headers':
1231 content = headers
1232 elif content == 'echo_request_headers_as_json':
1233 content = simplejson.dumps(headers)
1234 elif content == 'echo_request_body':
1235 content = body
1236 elif content == 'echo_request_uri':
1237 content = uri
1238 return httplib2.Response(resp), content
1239
1242 """Set the user-agent on every request.
1243
1244 Args:
1245 http - An instance of httplib2.Http
1246 or something that acts like it.
1247 user_agent: string, the value for the user-agent header.
1248
1249 Returns:
1250 A modified instance of http that was passed in.
1251
1252 Example:
1253
1254 h = httplib2.Http()
1255 h = set_user_agent(h, "my-app-name/6.0")
1256
1257 Most of the time the user-agent will be set doing auth, this is for the rare
1258 cases where you are accessing an unauthenticated endpoint.
1259 """
1260 request_orig = http.request
1261
1262
1263 def new_request(uri, method='GET', body=None, headers=None,
1264 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1265 connection_type=None):
1266 """Modify the request headers to add the user-agent."""
1267 if headers is None:
1268 headers = {}
1269 if 'user-agent' in headers:
1270 headers['user-agent'] = user_agent + ' ' + headers['user-agent']
1271 else:
1272 headers['user-agent'] = user_agent
1273 resp, content = request_orig(uri, method, body, headers,
1274 redirections, connection_type)
1275 return resp, content
1276
1277 http.request = new_request
1278 return http
1279
1282 """Tunnel PATCH requests over POST.
1283 Args:
1284 http - An instance of httplib2.Http
1285 or something that acts like it.
1286
1287 Returns:
1288 A modified instance of http that was passed in.
1289
1290 Example:
1291
1292 h = httplib2.Http()
1293 h = tunnel_patch(h, "my-app-name/6.0")
1294
1295 Useful if you are running on a platform that doesn't support PATCH.
1296 Apply this last if you are using OAuth 1.0, as changing the method
1297 will result in a different signature.
1298 """
1299 request_orig = http.request
1300
1301
1302 def new_request(uri, method='GET', body=None, headers=None,
1303 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
1304 connection_type=None):
1305 """Modify the request headers to add the user-agent."""
1306 if headers is None:
1307 headers = {}
1308 if method == 'PATCH':
1309 if 'oauth_token' in headers.get('authorization', ''):
1310 logging.warning(
1311 'OAuth 1.0 request made with Credentials after tunnel_patch.')
1312 headers['x-http-method-override'] = "PATCH"
1313 method = 'POST'
1314 resp, content = request_orig(uri, method, body, headers,
1315 redirections, connection_type)
1316 return resp, content
1317
1318 http.request = new_request
1319 return http
1320