Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(620)

Unified Diff: tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/base_api.py

Issue 1264873003: Add gsutil/third_party to telemetry/third_party/gsutilz/third_party. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Remove httplib2 Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/base_api.py
diff --git a/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/base_api.py b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/base_api.py
new file mode 100755
index 0000000000000000000000000000000000000000..d10314b67895b691b52d57bed31bfaaa8977bb41
--- /dev/null
+++ b/tools/telemetry/third_party/gsutilz/third_party/apitools/apitools/base/py/base_api.py
@@ -0,0 +1,628 @@
+#!/usr/bin/env python
+"""Base class for api services."""
+
+import contextlib
+import datetime
+import logging
+import pprint
+
+
+from protorpc import message_types
+from protorpc import messages
+import six
+from six.moves import http_client
+from six.moves import urllib
+
+
+from apitools.base.py import credentials_lib
+from apitools.base.py import encoding
+from apitools.base.py import exceptions
+from apitools.base.py import http_wrapper
+from apitools.base.py import util
+
+__all__ = [
+ 'ApiMethodInfo',
+ 'ApiUploadInfo',
+ 'BaseApiClient',
+ 'BaseApiService',
+ 'NormalizeApiEndpoint',
+]
+
+# TODO(craigcitro): Remove this once we quiet the spurious logging in
+# oauth2client (or drop oauth2client).
+logging.getLogger('oauth2client.util').setLevel(logging.ERROR)
+
+_MAX_URL_LENGTH = 2048
+
+
+class ApiUploadInfo(messages.Message):
+
+ """Media upload information for a method.
+
+ Fields:
+ accept: (repeated) MIME Media Ranges for acceptable media uploads
+ to this method.
+ max_size: (integer) Maximum size of a media upload, such as 3MB
+ or 1TB (converted to an integer).
+ resumable_path: Path to use for resumable uploads.
+ resumable_multipart: (boolean) Whether or not the resumable endpoint
+ supports multipart uploads.
+ simple_path: Path to use for simple uploads.
+ simple_multipart: (boolean) Whether or not the simple endpoint
+ supports multipart uploads.
+ """
+ accept = messages.StringField(1, repeated=True)
+ max_size = messages.IntegerField(2)
+ resumable_path = messages.StringField(3)
+ resumable_multipart = messages.BooleanField(4)
+ simple_path = messages.StringField(5)
+ simple_multipart = messages.BooleanField(6)
+
+
+class ApiMethodInfo(messages.Message):
+
+ """Configuration info for an API method.
+
+ All fields are strings unless noted otherwise.
+
+ Fields:
+ relative_path: Relative path for this method.
+ method_id: ID for this method.
+ http_method: HTTP verb to use for this method.
+ path_params: (repeated) path parameters for this method.
+ query_params: (repeated) query parameters for this method.
+ ordered_params: (repeated) ordered list of parameters for
+ this method.
+ description: description of this method.
+ request_type_name: name of the request type.
+ response_type_name: name of the response type.
+ request_field: if not null, the field to pass as the body
+ of this POST request. may also be the REQUEST_IS_BODY
+ value below to indicate the whole message is the body.
+ upload_config: (ApiUploadInfo) Information about the upload
+ configuration supported by this method.
+ supports_download: (boolean) If True, this method supports
+ downloading the request via the `alt=media` query
+ parameter.
+ """
+
+ relative_path = messages.StringField(1)
+ method_id = messages.StringField(2)
+ http_method = messages.StringField(3)
+ path_params = messages.StringField(4, repeated=True)
+ query_params = messages.StringField(5, repeated=True)
+ ordered_params = messages.StringField(6, repeated=True)
+ description = messages.StringField(7)
+ request_type_name = messages.StringField(8)
+ response_type_name = messages.StringField(9)
+ request_field = messages.StringField(10, default='')
+ upload_config = messages.MessageField(ApiUploadInfo, 11)
+ supports_download = messages.BooleanField(12, default=False)
+REQUEST_IS_BODY = '<request>'
+
+
+def _LoadClass(name, messages_module):
+ if name.startswith('message_types.'):
+ _, _, classname = name.partition('.')
+ return getattr(message_types, classname)
+ elif '.' not in name:
+ return getattr(messages_module, name)
+ else:
+ raise exceptions.GeneratedClientError('Unknown class %s' % name)
+
+
+def _RequireClassAttrs(obj, attrs):
+ for attr in attrs:
+ attr_name = attr.upper()
+ if not hasattr(obj, '%s' % attr_name) or not getattr(obj, attr_name):
+ msg = 'No %s specified for object of class %s.' % (
+ attr_name, type(obj).__name__)
+ raise exceptions.GeneratedClientError(msg)
+
+
+def NormalizeApiEndpoint(api_endpoint):
+ if not api_endpoint.endswith('/'):
+ api_endpoint += '/'
+ return api_endpoint
+
+
+class _UrlBuilder(object):
+
+ """Convenient container for url data."""
+
+ def __init__(self, base_url, relative_path=None, query_params=None):
+ components = urllib.parse.urlsplit(urllib.parse.urljoin(
+ base_url, relative_path or ''))
+ if components.fragment:
+ raise exceptions.ConfigurationValueError(
+ 'Unexpected url fragment: %s' % components.fragment)
+ self.query_params = urllib.parse.parse_qs(components.query or '')
+ if query_params is not None:
+ self.query_params.update(query_params)
+ self.__scheme = components.scheme
+ self.__netloc = components.netloc
+ self.relative_path = components.path or ''
+
+ @classmethod
+ def FromUrl(cls, url):
+ urlparts = urllib.parse.urlsplit(url)
+ query_params = urllib.parse.parse_qs(urlparts.query)
+ base_url = urllib.parse.urlunsplit((
+ urlparts.scheme, urlparts.netloc, '', None, None))
+ relative_path = urlparts.path or ''
+ return cls(
+ base_url, relative_path=relative_path, query_params=query_params)
+
+ @property
+ def base_url(self):
+ return urllib.parse.urlunsplit(
+ (self.__scheme, self.__netloc, '', '', ''))
+
+ @base_url.setter
+ def base_url(self, value):
+ components = urllib.parse.urlsplit(value)
+ if components.path or components.query or components.fragment:
+ raise exceptions.ConfigurationValueError(
+ 'Invalid base url: %s' % value)
+ self.__scheme = components.scheme
+ self.__netloc = components.netloc
+
+ @property
+ def query(self):
+ # TODO(craigcitro): In the case that some of the query params are
+ # non-ASCII, we may silently fail to encode correctly. We should
+ # figure out who is responsible for owning the object -> str
+ # conversion.
+ return urllib.parse.urlencode(self.query_params, doseq=True)
+
+ @property
+ def url(self):
+ if '{' in self.relative_path or '}' in self.relative_path:
+ raise exceptions.ConfigurationValueError(
+ 'Cannot create url with relative path %s' % self.relative_path)
+ return urllib.parse.urlunsplit((
+ self.__scheme, self.__netloc, self.relative_path, self.query, ''))
+
+
+class BaseApiClient(object):
+
+ """Base class for client libraries."""
+ MESSAGES_MODULE = None
+
+ _API_KEY = ''
+ _CLIENT_ID = ''
+ _CLIENT_SECRET = ''
+ _PACKAGE = ''
+ _SCOPES = []
+ _USER_AGENT = ''
+
+ def __init__(self, url, credentials=None, get_credentials=True, http=None,
+ model=None, log_request=False, log_response=False,
+ num_retries=5, credentials_args=None,
+ default_global_params=None, additional_http_headers=None):
+ _RequireClassAttrs(self, ('_package', '_scopes', 'messages_module'))
+ if default_global_params is not None:
+ util.Typecheck(default_global_params, self.params_type)
+ self.__default_global_params = default_global_params
+ self.log_request = log_request
+ self.log_response = log_response
+ self.__num_retries = 5
+ # We let the @property machinery below do our validation.
+ self.num_retries = num_retries
+ self._credentials = credentials
+ if get_credentials and not credentials:
+ credentials_args = credentials_args or {}
+ self._SetCredentials(**credentials_args)
+ self._url = NormalizeApiEndpoint(url)
+ self._http = http or http_wrapper.GetHttp()
+ # Note that "no credentials" is totally possible.
+ if self._credentials is not None:
+ self._http = self._credentials.authorize(self._http)
+ # TODO(craigcitro): Remove this field when we switch to proto2.
+ self.__include_fields = None
+
+ self.additional_http_headers = additional_http_headers or {}
+
+ # TODO(craigcitro): Finish deprecating these fields.
+ _ = model
+
+ self.__response_type_model = 'proto'
+
+ def _SetCredentials(self, **kwds):
+ """Fetch credentials, and set them for this client.
+
+ Note that we can't simply return credentials, since creating them
+ may involve side-effecting self.
+
+ Args:
+ **kwds: Additional keyword arguments are passed on to GetCredentials.
+
+ Returns:
+ None. Sets self._credentials.
+ """
+ args = {
+ 'api_key': self._API_KEY,
+ 'client': self,
+ 'client_id': self._CLIENT_ID,
+ 'client_secret': self._CLIENT_SECRET,
+ 'package_name': self._PACKAGE,
+ 'scopes': self._SCOPES,
+ 'user_agent': self._USER_AGENT,
+ }
+ args.update(kwds)
+ # TODO(craigcitro): It's a bit dangerous to pass this
+ # still-half-initialized self into this method, but we might need
+ # to set attributes on it associated with our credentials.
+ # Consider another way around this (maybe a callback?) and whether
+ # or not it's worth it.
+ self._credentials = credentials_lib.GetCredentials(**args)
+
+ @classmethod
+ def ClientInfo(cls):
+ return {
+ 'client_id': cls._CLIENT_ID,
+ 'client_secret': cls._CLIENT_SECRET,
+ 'scope': ' '.join(sorted(util.NormalizeScopes(cls._SCOPES))),
+ 'user_agent': cls._USER_AGENT,
+ }
+
+ @property
+ def base_model_class(self):
+ return None
+
+ @property
+ def http(self):
+ return self._http
+
+ @property
+ def url(self):
+ return self._url
+
+ @classmethod
+ def GetScopes(cls):
+ return cls._SCOPES
+
+ @property
+ def params_type(self):
+ return _LoadClass('StandardQueryParameters', self.MESSAGES_MODULE)
+
+ @property
+ def user_agent(self):
+ return self._USER_AGENT
+
+ @property
+ def _default_global_params(self):
+ if self.__default_global_params is None:
+ self.__default_global_params = self.params_type()
+ return self.__default_global_params
+
+ def AddGlobalParam(self, name, value):
+ params = self._default_global_params
+ setattr(params, name, value)
+
+ @property
+ def global_params(self):
+ return encoding.CopyProtoMessage(self._default_global_params)
+
+ @contextlib.contextmanager
+ def IncludeFields(self, include_fields):
+ self.__include_fields = include_fields
+ yield
+ self.__include_fields = None
+
+ @property
+ def response_type_model(self):
+ return self.__response_type_model
+
+ @contextlib.contextmanager
+ def JsonResponseModel(self):
+ """In this context, return raw JSON instead of proto."""
+ old_model = self.response_type_model
+ self.__response_type_model = 'json'
+ yield
+ self.__response_type_model = old_model
+
+ @property
+ def num_retries(self):
+ return self.__num_retries
+
+ @num_retries.setter
+ def num_retries(self, value):
+ util.Typecheck(value, six.integer_types)
+ if value < 0:
+ raise exceptions.InvalidDataError(
+ 'Cannot have negative value for num_retries')
+ self.__num_retries = value
+
+ @contextlib.contextmanager
+ def WithRetries(self, num_retries):
+ old_num_retries = self.num_retries
+ self.num_retries = num_retries
+ yield
+ self.num_retries = old_num_retries
+
+ def ProcessRequest(self, method_config, request):
+ """Hook for pre-processing of requests."""
+ if self.log_request:
+ logging.info(
+ 'Calling method %s with %s: %s', method_config.method_id,
+ method_config.request_type_name, request)
+ return request
+
+ def ProcessHttpRequest(self, http_request):
+ """Hook for pre-processing of http requests."""
+ http_request.headers.update(self.additional_http_headers)
+ if self.log_request:
+ logging.info('Making http %s to %s',
+ http_request.http_method, http_request.url)
+ logging.info('Headers: %s', pprint.pformat(http_request.headers))
+ if http_request.body:
+ # TODO(craigcitro): Make this safe to print in the case of
+ # non-printable body characters.
+ logging.info('Body:\n%s',
+ http_request.loggable_body or http_request.body)
+ else:
+ logging.info('Body: (none)')
+ return http_request
+
+ def ProcessResponse(self, method_config, response):
+ if self.log_response:
+ logging.info('Response of type %s: %s',
+ method_config.response_type_name, response)
+ return response
+
+ # TODO(craigcitro): Decide where these two functions should live.
+ def SerializeMessage(self, message):
+ return encoding.MessageToJson(
+ message, include_fields=self.__include_fields)
+
+ def DeserializeMessage(self, response_type, data):
+ """Deserialize the given data as method_config.response_type."""
+ try:
+ message = encoding.JsonToMessage(response_type, data)
+ except (exceptions.InvalidDataFromServerError,
+ messages.ValidationError) as e:
+ raise exceptions.InvalidDataFromServerError(
+ 'Error decoding response "%s" as type %s: %s' % (
+ data, response_type.__name__, e))
+ return message
+
+ def FinalizeTransferUrl(self, url):
+ """Modify the url for a given transfer, based on auth and version."""
+ url_builder = _UrlBuilder.FromUrl(url)
+ if self.global_params.key:
+ url_builder.query_params['key'] = self.global_params.key
+ return url_builder.url
+
+
+class BaseApiService(object):
+
+ """Base class for generated API services."""
+
+ def __init__(self, client):
+ self.__client = client
+ self._method_configs = {}
+ self._upload_configs = {}
+
+ @property
+ def _client(self):
+ return self.__client
+
+ @property
+ def client(self):
+ return self.__client
+
+ def GetMethodConfig(self, method):
+ return self._method_configs[method]
+
+ def GetUploadConfig(self, method):
+ return self._upload_configs.get(method)
+
+ def GetRequestType(self, method):
+ method_config = self.GetMethodConfig(method)
+ return getattr(self.client.MESSAGES_MODULE,
+ method_config.request_type_name)
+
+ def GetResponseType(self, method):
+ method_config = self.GetMethodConfig(method)
+ return getattr(self.client.MESSAGES_MODULE,
+ method_config.response_type_name)
+
+ def __CombineGlobalParams(self, global_params, default_params):
+ """Combine the given params with the defaults."""
+ util.Typecheck(global_params, (type(None), self.__client.params_type))
+ result = self.__client.params_type()
+ global_params = global_params or self.__client.params_type()
+ for field in result.all_fields():
+ value = global_params.get_assigned_value(field.name)
+ if value is None:
+ value = default_params.get_assigned_value(field.name)
+ if value not in (None, [], ()):
+ setattr(result, field.name, value)
+ return result
+
+ def __EncodePrettyPrint(self, query_info):
+ # The prettyPrint flag needs custom encoding: it should be encoded
+ # as 0 if False, and ignored otherwise (True is the default).
+ if not query_info.pop('prettyPrint', True):
+ query_info['prettyPrint'] = 0
+ # The One Platform equivalent of prettyPrint is pp, which also needs
+ # custom encoding.
+ if not query_info.pop('pp', True):
+ query_info['pp'] = 0
+ return query_info
+
+ def __ConstructQueryParams(self, query_params, request, global_params):
+ """Construct a dictionary of query parameters for this request."""
+ # First, handle the global params.
+ global_params = self.__CombineGlobalParams(
+ global_params, self.__client.global_params)
+ global_param_names = util.MapParamNames(
+ [x.name for x in self.__client.params_type.all_fields()],
+ self.__client.params_type)
+ query_info = dict((param, getattr(global_params, param))
+ for param in global_param_names)
+ # Next, add the query params.
+ query_param_names = util.MapParamNames(query_params, type(request))
+ query_info.update((param, getattr(request, param, None))
+ for param in query_param_names)
+ query_info = dict((k, v) for k, v in query_info.items()
+ if v is not None)
+ query_info = self.__EncodePrettyPrint(query_info)
+ query_info = util.MapRequestParams(query_info, type(request))
+ for k, v in query_info.items():
+ if isinstance(v, six.text_type):
+ query_info[k] = v.encode('utf8')
+ elif isinstance(v, str):
+ query_info[k] = v.decode('utf8')
+ elif isinstance(v, datetime.datetime):
+ query_info[k] = v.isoformat()
+ return query_info
+
+ def __ConstructRelativePath(self, method_config, request,
+ relative_path=None):
+ """Determine the relative path for request."""
+ python_param_names = util.MapParamNames(
+ method_config.path_params, type(request))
+ params = dict([(param, getattr(request, param, None))
+ for param in python_param_names])
+ params = util.MapRequestParams(params, type(request))
+ return util.ExpandRelativePath(method_config, params,
+ relative_path=relative_path)
+
+ def __FinalizeRequest(self, http_request, url_builder):
+ """Make any final general adjustments to the request."""
+ if (http_request.http_method == 'GET' and
+ len(http_request.url) > _MAX_URL_LENGTH):
+ http_request.http_method = 'POST'
+ http_request.headers['x-http-method-override'] = 'GET'
+ http_request.headers[
+ 'content-type'] = 'application/x-www-form-urlencoded'
+ http_request.body = url_builder.query
+ url_builder.query_params = {}
+ http_request.url = url_builder.url
+
+ def __ProcessHttpResponse(self, method_config, http_response):
+ """Process the given http response."""
+ if http_response.status_code not in (http_client.OK,
+ http_client.NO_CONTENT):
+ raise exceptions.HttpError.FromResponse(http_response)
+ if http_response.status_code == http_client.NO_CONTENT:
+ # TODO(craigcitro): Find out why _replace doesn't seem to work
+ # here.
+ http_response = http_wrapper.Response(
+ info=http_response.info, content='{}',
+ request_url=http_response.request_url)
+ if self.__client.response_type_model == 'json':
+ return http_response.content
+ else:
+ response_type = _LoadClass(method_config.response_type_name,
+ self.__client.MESSAGES_MODULE)
+ return self.__client.DeserializeMessage(
+ response_type, http_response.content)
+
+ def __SetBaseHeaders(self, http_request, client):
+ """Fill in the basic headers on http_request."""
+ # TODO(craigcitro): Make the default a little better here, and
+ # include the apitools version.
+ user_agent = client.user_agent or 'apitools-client/1.0'
+ http_request.headers['user-agent'] = user_agent
+ http_request.headers['accept'] = 'application/json'
+ http_request.headers['accept-encoding'] = 'gzip, deflate'
+
+ def __SetBody(self, http_request, method_config, request, upload):
+ """Fill in the body on http_request."""
+ if not method_config.request_field:
+ return
+
+ request_type = _LoadClass(
+ method_config.request_type_name, self.__client.MESSAGES_MODULE)
+ if method_config.request_field == REQUEST_IS_BODY:
+ body_value = request
+ body_type = request_type
+ else:
+ body_value = getattr(request, method_config.request_field)
+ body_field = request_type.field_by_name(
+ method_config.request_field)
+ util.Typecheck(body_field, messages.MessageField)
+ body_type = body_field.type
+
+ if upload and not body_value:
+ # We're going to fill in the body later.
+ return
+ util.Typecheck(body_value, body_type)
+ http_request.headers['content-type'] = 'application/json'
+ http_request.body = self.__client.SerializeMessage(body_value)
+
+ def PrepareHttpRequest(self, method_config, request, global_params=None,
+ upload=None, upload_config=None, download=None):
+ """Prepares an HTTP request to be sent."""
+ request_type = _LoadClass(
+ method_config.request_type_name, self.__client.MESSAGES_MODULE)
+ util.Typecheck(request, request_type)
+ request = self.__client.ProcessRequest(method_config, request)
+
+ http_request = http_wrapper.Request(
+ http_method=method_config.http_method)
+ self.__SetBaseHeaders(http_request, self.__client)
+ self.__SetBody(http_request, method_config, request, upload)
+
+ url_builder = _UrlBuilder(
+ self.__client.url, relative_path=method_config.relative_path)
+ url_builder.query_params = self.__ConstructQueryParams(
+ method_config.query_params, request, global_params)
+
+ # It's important that upload and download go before we fill in the
+ # relative path, so that they can replace it.
+ if upload is not None:
+ upload.ConfigureRequest(upload_config, http_request, url_builder)
+ if download is not None:
+ download.ConfigureRequest(http_request, url_builder)
+
+ url_builder.relative_path = self.__ConstructRelativePath(
+ method_config, request, relative_path=url_builder.relative_path)
+ self.__FinalizeRequest(http_request, url_builder)
+
+ return self.__client.ProcessHttpRequest(http_request)
+
+ def _RunMethod(self, method_config, request, global_params=None,
+ upload=None, upload_config=None, download=None):
+ """Call this method with request."""
+ if upload is not None and download is not None:
+ # TODO(craigcitro): This just involves refactoring the logic
+ # below into callbacks that we can pass around; in particular,
+ # the order should be that the upload gets the initial request,
+ # and then passes its reply to a download if one exists, and
+ # then that goes to ProcessResponse and is returned.
+ raise exceptions.NotYetImplementedError(
+ 'Cannot yet use both upload and download at once')
+
+ http_request = self.PrepareHttpRequest(
+ method_config, request, global_params, upload, upload_config,
+ download)
+
+ # TODO(craigcitro): Make num_retries customizable on Transfer
+ # objects, and pass in self.__client.num_retries when initializing
+ # an upload or download.
+ if download is not None:
+ download.InitializeDownload(http_request, client=self.client)
+ return
+
+ http_response = None
+ if upload is not None:
+ http_response = upload.InitializeUpload(
+ http_request, client=self.client)
+ if http_response is None:
+ http = self.__client.http
+ if upload and upload.bytes_http:
+ http = upload.bytes_http
+ http_response = http_wrapper.MakeRequest(
+ http, http_request, retries=self.__client.num_retries)
+
+ return self.ProcessHttpResponse(method_config, http_response)
+
+ def ProcessHttpResponse(self, method_config, http_response):
+ """Convert an HTTP response to the expected message type."""
+ return self.__client.ProcessResponse(
+ method_config,
+ self.__ProcessHttpResponse(method_config, http_response))

Powered by Google App Engine
This is Rietveld 408576698