• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2015 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Base class for api services."""
18
19import base64
20import contextlib
21import datetime
22import logging
23import pprint
24
25
26import six
27from six.moves import http_client
28from six.moves import urllib
29
30
31from apitools.base.protorpclite import message_types
32from apitools.base.protorpclite import messages
33from apitools.base.py import encoding
34from apitools.base.py import exceptions
35from apitools.base.py import http_wrapper
36from apitools.base.py import util
37
38__all__ = [
39    'ApiMethodInfo',
40    'ApiUploadInfo',
41    'BaseApiClient',
42    'BaseApiService',
43    'NormalizeApiEndpoint',
44]
45
46# TODO(craigcitro): Remove this once we quiet the spurious logging in
47# oauth2client (or drop oauth2client).
48logging.getLogger('oauth2client.util').setLevel(logging.ERROR)
49
50_MAX_URL_LENGTH = 2048
51
52
53class ApiUploadInfo(messages.Message):
54
55    """Media upload information for a method.
56
57    Fields:
58      accept: (repeated) MIME Media Ranges for acceptable media uploads
59          to this method.
60      max_size: (integer) Maximum size of a media upload, such as 3MB
61          or 1TB (converted to an integer).
62      resumable_path: Path to use for resumable uploads.
63      resumable_multipart: (boolean) Whether or not the resumable endpoint
64          supports multipart uploads.
65      simple_path: Path to use for simple uploads.
66      simple_multipart: (boolean) Whether or not the simple endpoint
67          supports multipart uploads.
68    """
69    accept = messages.StringField(1, repeated=True)
70    max_size = messages.IntegerField(2)
71    resumable_path = messages.StringField(3)
72    resumable_multipart = messages.BooleanField(4)
73    simple_path = messages.StringField(5)
74    simple_multipart = messages.BooleanField(6)
75
76
77class ApiMethodInfo(messages.Message):
78
79    """Configuration info for an API method.
80
81    All fields are strings unless noted otherwise.
82
83    Fields:
84      relative_path: Relative path for this method.
85      flat_path: Expanded version (if any) of relative_path.
86      method_id: ID for this method.
87      http_method: HTTP verb to use for this method.
88      path_params: (repeated) path parameters for this method.
89      query_params: (repeated) query parameters for this method.
90      ordered_params: (repeated) ordered list of parameters for
91          this method.
92      description: description of this method.
93      request_type_name: name of the request type.
94      response_type_name: name of the response type.
95      request_field: if not null, the field to pass as the body
96          of this POST request. may also be the REQUEST_IS_BODY
97          value below to indicate the whole message is the body.
98      upload_config: (ApiUploadInfo) Information about the upload
99          configuration supported by this method.
100      supports_download: (boolean) If True, this method supports
101          downloading the request via the `alt=media` query
102          parameter.
103    """
104
105    relative_path = messages.StringField(1)
106    flat_path = messages.StringField(2)
107    method_id = messages.StringField(3)
108    http_method = messages.StringField(4)
109    path_params = messages.StringField(5, repeated=True)
110    query_params = messages.StringField(6, repeated=True)
111    ordered_params = messages.StringField(7, repeated=True)
112    description = messages.StringField(8)
113    request_type_name = messages.StringField(9)
114    response_type_name = messages.StringField(10)
115    request_field = messages.StringField(11, default='')
116    upload_config = messages.MessageField(ApiUploadInfo, 12)
117    supports_download = messages.BooleanField(13, default=False)
118
119
120REQUEST_IS_BODY = '<request>'
121
122
123def _LoadClass(name, messages_module):
124    if name.startswith('message_types.'):
125        _, _, classname = name.partition('.')
126        return getattr(message_types, classname)
127    elif '.' not in name:
128        return getattr(messages_module, name)
129    else:
130        raise exceptions.GeneratedClientError('Unknown class %s' % name)
131
132
133def _RequireClassAttrs(obj, attrs):
134    for attr in attrs:
135        attr_name = attr.upper()
136        if not hasattr(obj, '%s' % attr_name) or not getattr(obj, attr_name):
137            msg = 'No %s specified for object of class %s.' % (
138                attr_name, type(obj).__name__)
139            raise exceptions.GeneratedClientError(msg)
140
141
142def NormalizeApiEndpoint(api_endpoint):
143    if not api_endpoint.endswith('/'):
144        api_endpoint += '/'
145    return api_endpoint
146
147
148def _urljoin(base, url):  # pylint: disable=invalid-name
149    """Custom urljoin replacement supporting : before / in url."""
150    # In general, it's unsafe to simply join base and url. However, for
151    # the case of discovery documents, we know:
152    #  * base will never contain params, query, or fragment
153    #  * url will never contain a scheme or net_loc.
154    # In general, this means we can safely join on /; we just need to
155    # ensure we end up with precisely one / joining base and url. The
156    # exception here is the case of media uploads, where url will be an
157    # absolute url.
158    if url.startswith('http://') or url.startswith('https://'):
159        return urllib.parse.urljoin(base, url)
160    new_base = base if base.endswith('/') else base + '/'
161    new_url = url[1:] if url.startswith('/') else url
162    return new_base + new_url
163
164
165class _UrlBuilder(object):
166
167    """Convenient container for url data."""
168
169    def __init__(self, base_url, relative_path=None, query_params=None):
170        components = urllib.parse.urlsplit(_urljoin(
171            base_url, relative_path or ''))
172        if components.fragment:
173            raise exceptions.ConfigurationValueError(
174                'Unexpected url fragment: %s' % components.fragment)
175        self.query_params = urllib.parse.parse_qs(components.query or '')
176        if query_params is not None:
177            self.query_params.update(query_params)
178        self.__scheme = components.scheme
179        self.__netloc = components.netloc
180        self.relative_path = components.path or ''
181
182    @classmethod
183    def FromUrl(cls, url):
184        urlparts = urllib.parse.urlsplit(url)
185        query_params = urllib.parse.parse_qs(urlparts.query)
186        base_url = urllib.parse.urlunsplit((
187            urlparts.scheme, urlparts.netloc, '', None, None))
188        relative_path = urlparts.path or ''
189        return cls(
190            base_url, relative_path=relative_path, query_params=query_params)
191
192    @property
193    def base_url(self):
194        return urllib.parse.urlunsplit(
195            (self.__scheme, self.__netloc, '', '', ''))
196
197    @base_url.setter
198    def base_url(self, value):
199        components = urllib.parse.urlsplit(value)
200        if components.path or components.query or components.fragment:
201            raise exceptions.ConfigurationValueError(
202                'Invalid base url: %s' % value)
203        self.__scheme = components.scheme
204        self.__netloc = components.netloc
205
206    @property
207    def query(self):
208        # TODO(craigcitro): In the case that some of the query params are
209        # non-ASCII, we may silently fail to encode correctly. We should
210        # figure out who is responsible for owning the object -> str
211        # conversion.
212        return urllib.parse.urlencode(self.query_params, True)
213
214    @property
215    def url(self):
216        if '{' in self.relative_path or '}' in self.relative_path:
217            raise exceptions.ConfigurationValueError(
218                'Cannot create url with relative path %s' % self.relative_path)
219        return urllib.parse.urlunsplit((
220            self.__scheme, self.__netloc, self.relative_path, self.query, ''))
221
222
223def _SkipGetCredentials():
224    """Hook for skipping credentials. For internal use."""
225    return False
226
227
228class BaseApiClient(object):
229
230    """Base class for client libraries."""
231    MESSAGES_MODULE = None
232
233    _API_KEY = ''
234    _CLIENT_ID = ''
235    _CLIENT_SECRET = ''
236    _PACKAGE = ''
237    _SCOPES = []
238    _USER_AGENT = ''
239
240    def __init__(self, url, credentials=None, get_credentials=True, http=None,
241                 model=None, log_request=False, log_response=False,
242                 num_retries=5, max_retry_wait=60, credentials_args=None,
243                 default_global_params=None, additional_http_headers=None,
244                 check_response_func=None, retry_func=None,
245                 response_encoding=None):
246        _RequireClassAttrs(self, ('_package', '_scopes', 'messages_module'))
247        if default_global_params is not None:
248            util.Typecheck(default_global_params, self.params_type)
249        self.__default_global_params = default_global_params
250        self.log_request = log_request
251        self.log_response = log_response
252        self.__num_retries = 5
253        self.__max_retry_wait = 60
254        # We let the @property machinery below do our validation.
255        self.num_retries = num_retries
256        self.max_retry_wait = max_retry_wait
257        self._credentials = credentials
258        get_credentials = get_credentials and not _SkipGetCredentials()
259        if get_credentials and not credentials:
260            credentials_args = credentials_args or {}
261            self._SetCredentials(**credentials_args)
262        self._url = NormalizeApiEndpoint(url)
263        self._http = http or http_wrapper.GetHttp()
264        # Note that "no credentials" is totally possible.
265        if self._credentials is not None:
266            self._http = self._credentials.authorize(self._http)
267        # TODO(craigcitro): Remove this field when we switch to proto2.
268        self.__include_fields = None
269
270        self.additional_http_headers = additional_http_headers or {}
271        self.check_response_func = check_response_func
272        self.retry_func = retry_func
273        self.response_encoding = response_encoding
274        # Since we can't change the init arguments without regenerating clients,
275        # offer this hook to affect FinalizeTransferUrl behavior.
276        self.overwrite_transfer_urls_with_client_base = False
277
278        # TODO(craigcitro): Finish deprecating these fields.
279        _ = model
280
281        self.__response_type_model = 'proto'
282
283    def _SetCredentials(self, **kwds):
284        """Fetch credentials, and set them for this client.
285
286        Note that we can't simply return credentials, since creating them
287        may involve side-effecting self.
288
289        Args:
290          **kwds: Additional keyword arguments are passed on to GetCredentials.
291
292        Returns:
293          None. Sets self._credentials.
294        """
295        args = {
296            'api_key': self._API_KEY,
297            'client': self,
298            'client_id': self._CLIENT_ID,
299            'client_secret': self._CLIENT_SECRET,
300            'package_name': self._PACKAGE,
301            'scopes': self._SCOPES,
302            'user_agent': self._USER_AGENT,
303        }
304        args.update(kwds)
305        # credentials_lib can be expensive to import so do it only if needed.
306        from apitools.base.py import credentials_lib
307        # TODO(craigcitro): It's a bit dangerous to pass this
308        # still-half-initialized self into this method, but we might need
309        # to set attributes on it associated with our credentials.
310        # Consider another way around this (maybe a callback?) and whether
311        # or not it's worth it.
312        self._credentials = credentials_lib.GetCredentials(**args)
313
314    @classmethod
315    def ClientInfo(cls):
316        return {
317            'client_id': cls._CLIENT_ID,
318            'client_secret': cls._CLIENT_SECRET,
319            'scope': ' '.join(sorted(util.NormalizeScopes(cls._SCOPES))),
320            'user_agent': cls._USER_AGENT,
321        }
322
323    @property
324    def base_model_class(self):
325        return None
326
327    @property
328    def http(self):
329        return self._http
330
331    @property
332    def url(self):
333        return self._url
334
335    @classmethod
336    def GetScopes(cls):
337        return cls._SCOPES
338
339    @property
340    def params_type(self):
341        return _LoadClass('StandardQueryParameters', self.MESSAGES_MODULE)
342
343    @property
344    def user_agent(self):
345        return self._USER_AGENT
346
347    @property
348    def _default_global_params(self):
349        if self.__default_global_params is None:
350            # pylint: disable=not-callable
351            self.__default_global_params = self.params_type()
352        return self.__default_global_params
353
354    def AddGlobalParam(self, name, value):
355        params = self._default_global_params
356        setattr(params, name, value)
357
358    @property
359    def global_params(self):
360        return encoding.CopyProtoMessage(self._default_global_params)
361
362    @contextlib.contextmanager
363    def IncludeFields(self, include_fields):
364        self.__include_fields = include_fields
365        yield
366        self.__include_fields = None
367
368    @property
369    def response_type_model(self):
370        return self.__response_type_model
371
372    @contextlib.contextmanager
373    def JsonResponseModel(self):
374        """In this context, return raw JSON instead of proto."""
375        old_model = self.response_type_model
376        self.__response_type_model = 'json'
377        yield
378        self.__response_type_model = old_model
379
380    @property
381    def num_retries(self):
382        return self.__num_retries
383
384    @num_retries.setter
385    def num_retries(self, value):
386        util.Typecheck(value, six.integer_types)
387        if value < 0:
388            raise exceptions.InvalidDataError(
389                'Cannot have negative value for num_retries')
390        self.__num_retries = value
391
392    @property
393    def max_retry_wait(self):
394        return self.__max_retry_wait
395
396    @max_retry_wait.setter
397    def max_retry_wait(self, value):
398        util.Typecheck(value, six.integer_types)
399        if value <= 0:
400            raise exceptions.InvalidDataError(
401                'max_retry_wait must be a postiive integer')
402        self.__max_retry_wait = value
403
404    @contextlib.contextmanager
405    def WithRetries(self, num_retries):
406        old_num_retries = self.num_retries
407        self.num_retries = num_retries
408        yield
409        self.num_retries = old_num_retries
410
411    def ProcessRequest(self, method_config, request):
412        """Hook for pre-processing of requests."""
413        if self.log_request:
414            logging.info(
415                'Calling method %s with %s: %s', method_config.method_id,
416                method_config.request_type_name, request)
417        return request
418
419    def ProcessHttpRequest(self, http_request):
420        """Hook for pre-processing of http requests."""
421        http_request.headers.update(self.additional_http_headers)
422        if self.log_request:
423            logging.info('Making http %s to %s',
424                         http_request.http_method, http_request.url)
425            logging.info('Headers: %s', pprint.pformat(http_request.headers))
426            if http_request.body:
427                # TODO(craigcitro): Make this safe to print in the case of
428                # non-printable body characters.
429                logging.info('Body:\n%s',
430                             http_request.loggable_body or http_request.body)
431            else:
432                logging.info('Body: (none)')
433        return http_request
434
435    def ProcessResponse(self, method_config, response):
436        if self.log_response:
437            logging.info('Response of type %s: %s',
438                         method_config.response_type_name, response)
439        return response
440
441    # TODO(craigcitro): Decide where these two functions should live.
442    def SerializeMessage(self, message):
443        return encoding.MessageToJson(
444            message, include_fields=self.__include_fields)
445
446    def DeserializeMessage(self, response_type, data):
447        """Deserialize the given data as method_config.response_type."""
448        try:
449            message = encoding.JsonToMessage(response_type, data)
450        except (exceptions.InvalidDataFromServerError,
451                messages.ValidationError, ValueError) as e:
452            raise exceptions.InvalidDataFromServerError(
453                'Error decoding response "%s" as type %s: %s' % (
454                    data, response_type.__name__, e))
455        return message
456
457    def FinalizeTransferUrl(self, url):
458        """Modify the url for a given transfer, based on auth and version."""
459        url_builder = _UrlBuilder.FromUrl(url)
460        if getattr(self.global_params, 'key', None):
461            url_builder.query_params['key'] = self.global_params.key
462        if self.overwrite_transfer_urls_with_client_base:
463            client_url_builder = _UrlBuilder.FromUrl(self._url)
464            url_builder.base_url = client_url_builder.base_url
465        return url_builder.url
466
467
468class BaseApiService(object):
469
470    """Base class for generated API services."""
471
472    def __init__(self, client):
473        self.__client = client
474        self._method_configs = {}
475        self._upload_configs = {}
476
477    @property
478    def _client(self):
479        return self.__client
480
481    @property
482    def client(self):
483        return self.__client
484
485    def GetMethodConfig(self, method):
486        """Returns service cached method config for given method."""
487        method_config = self._method_configs.get(method)
488        if method_config:
489            return method_config
490        func = getattr(self, method, None)
491        if func is None:
492            raise KeyError(method)
493        method_config = getattr(func, 'method_config', None)
494        if method_config is None:
495            raise KeyError(method)
496        self._method_configs[method] = config = method_config()
497        return config
498
499    @classmethod
500    def GetMethodsList(cls):
501        return [f.__name__ for f in six.itervalues(cls.__dict__)
502                if getattr(f, 'method_config', None)]
503
504    def GetUploadConfig(self, method):
505        return self._upload_configs.get(method)
506
507    def GetRequestType(self, method):
508        method_config = self.GetMethodConfig(method)
509        return getattr(self.client.MESSAGES_MODULE,
510                       method_config.request_type_name)
511
512    def GetResponseType(self, method):
513        method_config = self.GetMethodConfig(method)
514        return getattr(self.client.MESSAGES_MODULE,
515                       method_config.response_type_name)
516
517    def __CombineGlobalParams(self, global_params, default_params):
518        """Combine the given params with the defaults."""
519        util.Typecheck(global_params, (type(None), self.__client.params_type))
520        result = self.__client.params_type()
521        global_params = global_params or self.__client.params_type()
522        for field in result.all_fields():
523            value = global_params.get_assigned_value(field.name)
524            if value is None:
525                value = default_params.get_assigned_value(field.name)
526            if value not in (None, [], ()):
527                setattr(result, field.name, value)
528        return result
529
530    def __EncodePrettyPrint(self, query_info):
531        # The prettyPrint flag needs custom encoding: it should be encoded
532        # as 0 if False, and ignored otherwise (True is the default).
533        if not query_info.pop('prettyPrint', True):
534            query_info['prettyPrint'] = 0
535        # The One Platform equivalent of prettyPrint is pp, which also needs
536        # custom encoding.
537        if not query_info.pop('pp', True):
538            query_info['pp'] = 0
539        return query_info
540
541    def __FinalUrlValue(self, value, field):
542        """Encode value for the URL, using field to skip encoding for bytes."""
543        if isinstance(field, messages.BytesField) and value is not None:
544            return base64.urlsafe_b64encode(value)
545        elif isinstance(value, six.text_type):
546            return value.encode('utf8')
547        elif isinstance(value, six.binary_type):
548            return value.decode('utf8')
549        elif isinstance(value, datetime.datetime):
550            return value.isoformat()
551        return value
552
553    def __ConstructQueryParams(self, query_params, request, global_params):
554        """Construct a dictionary of query parameters for this request."""
555        # First, handle the global params.
556        global_params = self.__CombineGlobalParams(
557            global_params, self.__client.global_params)
558        global_param_names = util.MapParamNames(
559            [x.name for x in self.__client.params_type.all_fields()],
560            self.__client.params_type)
561        global_params_type = type(global_params)
562        query_info = dict(
563            (param,
564             self.__FinalUrlValue(getattr(global_params, param),
565                                  getattr(global_params_type, param)))
566            for param in global_param_names)
567        # Next, add the query params.
568        query_param_names = util.MapParamNames(query_params, type(request))
569        request_type = type(request)
570        query_info.update(
571            (param,
572             self.__FinalUrlValue(getattr(request, param, None),
573                                  getattr(request_type, param)))
574            for param in query_param_names)
575        query_info = dict((k, v) for k, v in query_info.items()
576                          if v is not None)
577        query_info = self.__EncodePrettyPrint(query_info)
578        query_info = util.MapRequestParams(query_info, type(request))
579        return query_info
580
581    def __ConstructRelativePath(self, method_config, request,
582                                relative_path=None):
583        """Determine the relative path for request."""
584        python_param_names = util.MapParamNames(
585            method_config.path_params, type(request))
586        params = dict([(param, getattr(request, param, None))
587                       for param in python_param_names])
588        params = util.MapRequestParams(params, type(request))
589        return util.ExpandRelativePath(method_config, params,
590                                       relative_path=relative_path)
591
592    def __FinalizeRequest(self, http_request, url_builder):
593        """Make any final general adjustments to the request."""
594        if (http_request.http_method == 'GET' and
595                len(http_request.url) > _MAX_URL_LENGTH):
596            http_request.http_method = 'POST'
597            http_request.headers['x-http-method-override'] = 'GET'
598            http_request.headers[
599                'content-type'] = 'application/x-www-form-urlencoded'
600            http_request.body = url_builder.query
601            url_builder.query_params = {}
602        http_request.url = url_builder.url
603
604    def __ProcessHttpResponse(self, method_config, http_response, request):
605        """Process the given http response."""
606        if http_response.status_code not in (http_client.OK,
607                                             http_client.CREATED,
608                                             http_client.NO_CONTENT):
609            raise exceptions.HttpError.FromResponse(
610                http_response, method_config=method_config, request=request)
611        if http_response.status_code == http_client.NO_CONTENT:
612            # TODO(craigcitro): Find out why _replace doesn't seem to work
613            # here.
614            http_response = http_wrapper.Response(
615                info=http_response.info, content='{}',
616                request_url=http_response.request_url)
617
618        content = http_response.content
619        if self._client.response_encoding and isinstance(content, bytes):
620            content = content.decode(self._client.response_encoding)
621
622        if self.__client.response_type_model == 'json':
623            return content
624        response_type = _LoadClass(method_config.response_type_name,
625                                   self.__client.MESSAGES_MODULE)
626        return self.__client.DeserializeMessage(response_type, content)
627
628    def __SetBaseHeaders(self, http_request, client):
629        """Fill in the basic headers on http_request."""
630        # TODO(craigcitro): Make the default a little better here, and
631        # include the apitools version.
632        user_agent = client.user_agent or 'apitools-client/1.0'
633        http_request.headers['user-agent'] = user_agent
634        http_request.headers['accept'] = 'application/json'
635        http_request.headers['accept-encoding'] = 'gzip, deflate'
636
637    def __SetBody(self, http_request, method_config, request, upload):
638        """Fill in the body on http_request."""
639        if not method_config.request_field:
640            return
641
642        request_type = _LoadClass(
643            method_config.request_type_name, self.__client.MESSAGES_MODULE)
644        if method_config.request_field == REQUEST_IS_BODY:
645            body_value = request
646            body_type = request_type
647        else:
648            body_value = getattr(request, method_config.request_field)
649            body_field = request_type.field_by_name(
650                method_config.request_field)
651            util.Typecheck(body_field, messages.MessageField)
652            body_type = body_field.type
653
654        # If there was no body provided, we use an empty message of the
655        # appropriate type.
656        body_value = body_value or body_type()
657        if upload and not body_value:
658            # We're going to fill in the body later.
659            return
660        util.Typecheck(body_value, body_type)
661        http_request.headers['content-type'] = 'application/json'
662        http_request.body = self.__client.SerializeMessage(body_value)
663
664    def PrepareHttpRequest(self, method_config, request, global_params=None,
665                           upload=None, upload_config=None, download=None):
666        """Prepares an HTTP request to be sent."""
667        request_type = _LoadClass(
668            method_config.request_type_name, self.__client.MESSAGES_MODULE)
669        util.Typecheck(request, request_type)
670        request = self.__client.ProcessRequest(method_config, request)
671
672        http_request = http_wrapper.Request(
673            http_method=method_config.http_method)
674        self.__SetBaseHeaders(http_request, self.__client)
675        self.__SetBody(http_request, method_config, request, upload)
676
677        url_builder = _UrlBuilder(
678            self.__client.url, relative_path=method_config.relative_path)
679        url_builder.query_params = self.__ConstructQueryParams(
680            method_config.query_params, request, global_params)
681
682        # It's important that upload and download go before we fill in the
683        # relative path, so that they can replace it.
684        if upload is not None:
685            upload.ConfigureRequest(upload_config, http_request, url_builder)
686        if download is not None:
687            download.ConfigureRequest(http_request, url_builder)
688
689        url_builder.relative_path = self.__ConstructRelativePath(
690            method_config, request, relative_path=url_builder.relative_path)
691        self.__FinalizeRequest(http_request, url_builder)
692
693        return self.__client.ProcessHttpRequest(http_request)
694
695    def _RunMethod(self, method_config, request, global_params=None,
696                   upload=None, upload_config=None, download=None):
697        """Call this method with request."""
698        if upload is not None and download is not None:
699            # TODO(craigcitro): This just involves refactoring the logic
700            # below into callbacks that we can pass around; in particular,
701            # the order should be that the upload gets the initial request,
702            # and then passes its reply to a download if one exists, and
703            # then that goes to ProcessResponse and is returned.
704            raise exceptions.NotYetImplementedError(
705                'Cannot yet use both upload and download at once')
706
707        http_request = self.PrepareHttpRequest(
708            method_config, request, global_params, upload, upload_config,
709            download)
710
711        # TODO(craigcitro): Make num_retries customizable on Transfer
712        # objects, and pass in self.__client.num_retries when initializing
713        # an upload or download.
714        if download is not None:
715            download.InitializeDownload(http_request, client=self.client)
716            return
717
718        http_response = None
719        if upload is not None:
720            http_response = upload.InitializeUpload(
721                http_request, client=self.client)
722        if http_response is None:
723            http = self.__client.http
724            if upload and upload.bytes_http:
725                http = upload.bytes_http
726            opts = {
727                'retries': self.__client.num_retries,
728                'max_retry_wait': self.__client.max_retry_wait,
729            }
730            if self.__client.check_response_func:
731                opts['check_response_func'] = self.__client.check_response_func
732            if self.__client.retry_func:
733                opts['retry_func'] = self.__client.retry_func
734            http_response = http_wrapper.MakeRequest(
735                http, http_request, **opts)
736
737        return self.ProcessHttpResponse(method_config, http_response, request)
738
739    def ProcessHttpResponse(self, method_config, http_response, request=None):
740        """Convert an HTTP response to the expected message type."""
741        return self.__client.ProcessResponse(
742            method_config,
743            self.__ProcessHttpResponse(method_config, http_response, request))
744