Index: tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/batch.py |
diff --git a/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/batch.py b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/batch.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..4910d4ac60ae418c49ce8b472b62383ace969349 |
--- /dev/null |
+++ b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/batch.py |
@@ -0,0 +1,467 @@ |
+#!/usr/bin/env python |
+"""Library for handling batch HTTP requests for apitools.""" |
+ |
+import collections |
+import email.generator as generator |
+import email.mime.multipart as mime_multipart |
+import email.mime.nonmultipart as mime_nonmultipart |
+import email.parser as email_parser |
+import itertools |
+import time |
+import uuid |
+ |
+import six |
+from six.moves import http_client |
+from six.moves import urllib_parse |
+ |
+from apitools.base.py import exceptions |
+from apitools.base.py import http_wrapper |
+ |
+__all__ = [ |
+ 'BatchApiRequest', |
+] |
+ |
+ |
+class RequestResponseAndHandler(collections.namedtuple( |
+ 'RequestResponseAndHandler', ['request', 'response', 'handler'])): |
+ |
+ """Container for data related to completing an HTTP request. |
+ |
+ This contains an HTTP request, its response, and a callback for handling |
+ the response from the server. |
+ |
+ Attributes: |
+ request: An http_wrapper.Request object representing the HTTP request. |
+ response: The http_wrapper.Response object returned from the server. |
+ handler: A callback function accepting two arguments, response |
+ and exception. Response is an http_wrapper.Response object, and |
+ exception is an apiclient.errors.HttpError object if an error |
+ occurred, or otherwise None. |
+ """ |
+ |
+ |
+class BatchApiRequest(object): |
+ |
+ class ApiCall(object): |
+ |
+ """Holds request and response information for each request. |
+ |
+ ApiCalls are ultimately exposed to the client once the HTTP |
+ batch request has been completed. |
+ |
+ Attributes: |
+ http_request: A client-supplied http_wrapper.Request to be |
+ submitted to the server. |
+ response: A http_wrapper.Response object given by the server as a |
+ response to the user request, or None if an error occurred. |
+ exception: An apiclient.errors.HttpError object if an error |
+ occurred, or None. |
+ |
+ """ |
+ |
+ def __init__(self, request, retryable_codes, service, method_config): |
+ """Initialize an individual API request. |
+ |
+ Args: |
+ request: An http_wrapper.Request object. |
+ retryable_codes: A list of integer HTTP codes that can |
+ be retried. |
+ service: A service inheriting from base_api.BaseApiService. |
+ method_config: Method config for the desired API request. |
+ |
+ """ |
+ self.__retryable_codes = list( |
+ set(retryable_codes + [http_client.UNAUTHORIZED])) |
+ self.__http_response = None |
+ self.__service = service |
+ self.__method_config = method_config |
+ |
+ self.http_request = request |
+ # TODO(user): Add some validation to these fields. |
+ self.__response = None |
+ self.__exception = None |
+ |
+ @property |
+ def is_error(self): |
+ return self.exception is not None |
+ |
+ @property |
+ def response(self): |
+ return self.__response |
+ |
+ @property |
+ def exception(self): |
+ return self.__exception |
+ |
+ @property |
+ def authorization_failed(self): |
+ return (self.__http_response and ( |
+ self.__http_response.status_code == http_client.UNAUTHORIZED)) |
+ |
+ @property |
+ def terminal_state(self): |
+ if self.__http_response is None: |
+ return False |
+ response_code = self.__http_response.status_code |
+ return response_code not in self.__retryable_codes |
+ |
+ def HandleResponse(self, http_response, exception): |
+ """Handles an incoming http response to the request in http_request. |
+ |
+ This is intended to be used as a callback function for |
+ BatchHttpRequest.Add. |
+ |
+ Args: |
+ http_response: Deserialized http_wrapper.Response object. |
+ exception: apiclient.errors.HttpError object if an error |
+ occurred. |
+ |
+ """ |
+ self.__http_response = http_response |
+ self.__exception = exception |
+ if self.terminal_state and not self.__exception: |
+ self.__response = self.__service.ProcessHttpResponse( |
+ self.__method_config, self.__http_response) |
+ |
+ def __init__(self, batch_url=None, retryable_codes=None): |
+ """Initialize a batch API request object. |
+ |
+ Args: |
+ batch_url: Base URL for batch API calls. |
+ retryable_codes: A list of integer HTTP codes that can be retried. |
+ """ |
+ self.api_requests = [] |
+ self.retryable_codes = retryable_codes or [] |
+ self.batch_url = batch_url or 'https://www.googleapis.com/batch' |
+ |
+ def Add(self, service, method, request, global_params=None): |
+ """Add a request to the batch. |
+ |
+ Args: |
+ service: A class inheriting base_api.BaseApiService. |
+ method: A string indicated desired method from the service. See |
+ the example in the class docstring. |
+ request: An input message appropriate for the specified |
+ service.method. |
+ global_params: Optional additional parameters to pass into |
+ method.PrepareHttpRequest. |
+ |
+ Returns: |
+ None |
+ |
+ """ |
+ # Retrieve the configs for the desired method and service. |
+ method_config = service.GetMethodConfig(method) |
+ upload_config = service.GetUploadConfig(method) |
+ |
+ # Prepare the HTTP Request. |
+ http_request = service.PrepareHttpRequest( |
+ method_config, request, global_params=global_params, |
+ upload_config=upload_config) |
+ |
+ # Create the request and add it to our master list. |
+ api_request = self.ApiCall( |
+ http_request, self.retryable_codes, service, method_config) |
+ self.api_requests.append(api_request) |
+ |
+ def Execute(self, http, sleep_between_polls=5, max_retries=5): |
+ """Execute all of the requests in the batch. |
+ |
+ Args: |
+ http: httplib2.Http object for use in the request. |
+ sleep_between_polls: Integer number of seconds to sleep between |
+ polls. |
+ max_retries: Max retries. Any requests that have not succeeded by |
+ this number of retries simply report the last response or |
+ exception, whatever it happened to be. |
+ |
+ Returns: |
+ List of ApiCalls. |
+ """ |
+ requests = [request for request in self.api_requests |
+ if not request.terminal_state] |
+ |
+ for attempt in range(max_retries): |
+ if attempt: |
+ time.sleep(sleep_between_polls) |
+ |
+ # Create a batch_http_request object and populate it with |
+ # incomplete requests. |
+ batch_http_request = BatchHttpRequest(batch_url=self.batch_url) |
+ for request in requests: |
+ batch_http_request.Add( |
+ request.http_request, request.HandleResponse) |
+ batch_http_request.Execute(http) |
+ |
+ # Collect retryable requests. |
+ requests = [request for request in self.api_requests if not |
+ request.terminal_state] |
+ |
+ if hasattr(http.request, 'credentials'): |
+ if any(request.authorization_failed for request in requests): |
+ http.request.credentials.refresh(http) |
+ |
+ if not requests: |
+ break |
+ |
+ return self.api_requests |
+ |
+ |
+class BatchHttpRequest(object): |
+ |
+ """Batches multiple http_wrapper.Request objects into a single request.""" |
+ |
+ def __init__(self, batch_url, callback=None): |
+ """Constructor for a BatchHttpRequest. |
+ |
+ Args: |
+ batch_url: URL to send batch requests to. |
+ callback: A callback to be called for each response, of the |
+ form callback(response, exception). The first parameter is |
+ the deserialized Response object. The second is an |
+ apiclient.errors.HttpError exception object if an HTTP error |
+ occurred while processing the request, or None if no error |
+ occurred. |
+ """ |
+ # Endpoint to which these requests are sent. |
+ self.__batch_url = batch_url |
+ |
+ # Global callback to be called for each individual response in the |
+ # batch. |
+ self.__callback = callback |
+ |
+ # List of requests, responses and handlers. |
+ self.__request_response_handlers = {} |
+ |
+ # The last auto generated id. |
+ self.__last_auto_id = itertools.count() |
+ |
+ # Unique ID on which to base the Content-ID headers. |
+ self.__base_id = uuid.uuid4() |
+ |
+ def _ConvertIdToHeader(self, request_id): |
+ """Convert an id to a Content-ID header value. |
+ |
+ Args: |
+ request_id: String identifier for a individual request. |
+ |
+ Returns: |
+ A Content-ID header with the id_ encoded into it. A UUID is |
+ prepended to the value because Content-ID headers are |
+ supposed to be universally unique. |
+ |
+ """ |
+ return '<%s+%s>' % (self.__base_id, urllib_parse.quote(request_id)) |
+ |
+ @staticmethod |
+ def _ConvertHeaderToId(header): |
+ """Convert a Content-ID header value to an id. |
+ |
+ Presumes the Content-ID header conforms to the format that |
+ _ConvertIdToHeader() returns. |
+ |
+ Args: |
+ header: A string indicating the Content-ID header value. |
+ |
+ Returns: |
+ The extracted id value. |
+ |
+ Raises: |
+ BatchError if the header is not in the expected format. |
+ """ |
+ if not (header.startswith('<') or header.endswith('>')): |
+ raise exceptions.BatchError( |
+ 'Invalid value for Content-ID: %s' % header) |
+ if '+' not in header: |
+ raise exceptions.BatchError( |
+ 'Invalid value for Content-ID: %s' % header) |
+ _, request_id = header[1:-1].rsplit('+', 1) |
+ |
+ return urllib_parse.unquote(request_id) |
+ |
+ def _SerializeRequest(self, request): |
+ """Convert a http_wrapper.Request object into a string. |
+ |
+ Args: |
+ request: A http_wrapper.Request to serialize. |
+ |
+ Returns: |
+ The request as a string in application/http format. |
+ """ |
+ # Construct status line |
+ parsed = urllib_parse.urlsplit(request.url) |
+ request_line = urllib_parse.urlunsplit( |
+ (None, None, parsed.path, parsed.query, None)) |
+ status_line = u' '.join(( |
+ request.http_method, |
+ request_line.decode('utf-8'), |
+ u'HTTP/1.1\n' |
+ )) |
+ major, minor = request.headers.get( |
+ 'content-type', 'application/json').split('/') |
+ msg = mime_nonmultipart.MIMENonMultipart(major, minor) |
+ |
+ # MIMENonMultipart adds its own Content-Type header. |
+ # Keep all of the other headers in `request.headers`. |
+ for key, value in request.headers.items(): |
+ if key == 'content-type': |
+ continue |
+ msg[key] = value |
+ |
+ msg['Host'] = parsed.netloc |
+ msg.set_unixfrom(None) |
+ |
+ if request.body is not None: |
+ msg.set_payload(request.body) |
+ |
+ # Serialize the mime message. |
+ str_io = six.StringIO() |
+ # maxheaderlen=0 means don't line wrap headers. |
+ gen = generator.Generator(str_io, maxheaderlen=0) |
+ gen.flatten(msg, unixfrom=False) |
+ body = str_io.getvalue() |
+ |
+ # Strip off the \n\n that the MIME lib tacks onto the end of the |
+ # payload. |
+ if request.body is None: |
+ body = body[:-2] |
+ |
+ return status_line + body |
+ |
+ def _DeserializeResponse(self, payload): |
+ """Convert string into Response and content. |
+ |
+ Args: |
+ payload: Header and body string to be deserialized. |
+ |
+ Returns: |
+ A Response object |
+ """ |
+ # Strip off the status line. |
+ status_line, payload = payload.split('\n', 1) |
+ _, status, _ = status_line.split(' ', 2) |
+ |
+ # Parse the rest of the response. |
+ parser = email_parser.Parser() |
+ msg = parser.parsestr(payload) |
+ |
+ # Get the headers. |
+ info = dict(msg) |
+ info['status'] = status |
+ |
+ # Create Response from the parsed headers. |
+ content = msg.get_payload() |
+ |
+ return http_wrapper.Response(info, content, self.__batch_url) |
+ |
+ def _NewId(self): |
+ """Create a new id. |
+ |
+ Auto incrementing number that avoids conflicts with ids already used. |
+ |
+ Returns: |
+ A new unique id string. |
+ """ |
+ return str(next(self.__last_auto_id)) |
+ |
+ def Add(self, request, callback=None): |
+ """Add a new request. |
+ |
+ Args: |
+ request: A http_wrapper.Request to add to the batch. |
+ callback: A callback to be called for this response, of the |
+ form callback(response, exception). The first parameter is the |
+ deserialized response object. The second is an |
+ apiclient.errors.HttpError exception object if an HTTP error |
+ occurred while processing the request, or None if no errors |
+ occurred. |
+ |
+ Returns: |
+ None |
+ """ |
+ handler = RequestResponseAndHandler(request, None, callback) |
+ self.__request_response_handlers[self._NewId()] = handler |
+ |
+ def _Execute(self, http): |
+ """Serialize batch request, send to server, process response. |
+ |
+ Args: |
+ http: A httplib2.Http object to be used to make the request with. |
+ |
+ Raises: |
+ httplib2.HttpLib2Error if a transport error has occured. |
+ apiclient.errors.BatchError if the response is the wrong format. |
+ """ |
+ message = mime_multipart.MIMEMultipart('mixed') |
+ # Message should not write out its own headers. |
+ setattr(message, '_write_headers', lambda self: None) |
+ |
+ # Add all the individual requests. |
+ for key in self.__request_response_handlers: |
+ msg = mime_nonmultipart.MIMENonMultipart('application', 'http') |
+ msg['Content-Transfer-Encoding'] = 'binary' |
+ msg['Content-ID'] = self._ConvertIdToHeader(key) |
+ |
+ body = self._SerializeRequest( |
+ self.__request_response_handlers[key].request) |
+ msg.set_payload(body) |
+ message.attach(msg) |
+ |
+ request = http_wrapper.Request(self.__batch_url, 'POST') |
+ request.body = message.as_string() |
+ request.headers['content-type'] = ( |
+ 'multipart/mixed; boundary="%s"') % message.get_boundary() |
+ |
+ response = http_wrapper.MakeRequest(http, request) |
+ |
+ if response.status_code >= 300: |
+ raise exceptions.HttpError.FromResponse(response) |
+ |
+ # Prepend with a content-type header so Parser can handle it. |
+ header = 'content-type: %s\r\n\r\n' % response.info['content-type'] |
+ |
+ parser = email_parser.Parser() |
+ mime_response = parser.parsestr(header + response.content) |
+ |
+ if not mime_response.is_multipart(): |
+ raise exceptions.BatchError( |
+ 'Response not in multipart/mixed format.') |
+ |
+ for part in mime_response.get_payload(): |
+ request_id = self._ConvertHeaderToId(part['Content-ID']) |
+ response = self._DeserializeResponse(part.get_payload()) |
+ |
+ # Disable protected access because namedtuple._replace(...) |
+ # is not actually meant to be protected. |
+ self.__request_response_handlers[request_id] = ( |
+ self.__request_response_handlers[request_id]._replace( |
+ response=response)) |
+ |
+ def Execute(self, http): |
+ """Execute all the requests as a single batched HTTP request. |
+ |
+ Args: |
+ http: A httplib2.Http object to be used with the request. |
+ |
+ Returns: |
+ None |
+ |
+ Raises: |
+ BatchError if the response is the wrong format. |
+ """ |
+ |
+ self._Execute(http) |
+ |
+ for key in self.__request_response_handlers: |
+ response = self.__request_response_handlers[key].response |
+ callback = self.__request_response_handlers[key].handler |
+ |
+ exception = None |
+ |
+ if response.status_code >= 300: |
+ exception = exceptions.HttpError.FromResponse(response) |
+ |
+ if callback is not None: |
+ callback(response, exception) |
+ if self.__callback is not None: |
+ self.__callback(response, exception) |