• 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
275        # TODO(craigcitro): Finish deprecating these fields.
276        _ = model
277
278        self.__response_type_model = 'proto'
279
280    def _SetCredentials(self, **kwds):
281        """Fetch credentials, and set them for this client.
282
283        Note that we can't simply return credentials, since creating them
284        may involve side-effecting self.
285
286        Args:
287          **kwds: Additional keyword arguments are passed on to GetCredentials.
288
289        Returns:
290          None. Sets self._credentials.
291        """
292        args = {
293            'api_key': self._API_KEY,
294            'client': self,
295            'client_id': self._CLIENT_ID,
296            'client_secret': self._CLIENT_SECRET,
297            'package_name': self._PACKAGE,
298            'scopes': self._SCOPES,
299            'user_agent': self._USER_AGENT,
300        }
301        args.update(kwds)
302        # credentials_lib can be expensive to import so do it only if needed.
303        from apitools.base.py import credentials_lib
304        # TODO(craigcitro): It's a bit dangerous to pass this
305        # still-half-initialized self into this method, but we might need
306        # to set attributes on it associated with our credentials.
307        # Consider another way around this (maybe a callback?) and whether
308        # or not it's worth it.
309        self._credentials = credentials_lib.GetCredentials(**args)
310
311    @classmethod
312    def ClientInfo(cls):
313        return {
314            'client_id': cls._CLIENT_ID,
315            'client_secret': cls._CLIENT_SECRET,
316            'scope': ' '.join(sorted(util.NormalizeScopes(cls._SCOPES))),
317            'user_agent': cls._USER_AGENT,
318        }
319
320    @property
321    def base_model_class(self):
322        return None
323
324    @property
325    def http(self):
326        return self._http
327
328    @property
329    def url(self):
330        return self._url
331
332    @classmethod
333    def GetScopes(cls):
334        return cls._SCOPES
335
336    @property
337    def params_type(self):
338        return _LoadClass('StandardQueryParameters', self.MESSAGES_MODULE)
339
340    @property
341    def user_agent(self):
342        return self._USER_AGENT
343
344    @property
345    def _default_global_params(self):
346        if self.__default_global_params is None:
347            # pylint: disable=not-callable
348            self.__default_global_params = self.params_type()
349        return self.__default_global_params
350
351    def AddGlobalParam(self, name, value):
352        params = self._default_global_params
353        setattr(params, name, value)
354
355    @property
356    def global_params(self):
357        return encoding.CopyProtoMessage(self._default_global_params)
358
359    @contextlib.contextmanager
360    def IncludeFields(self, include_fields):
361        self.__include_fields = include_fields
362        yield
363        self.__include_fields = None
364
365    @property
366    def response_type_model(self):
367        return self.__response_type_model
368
369    @contextlib.contextmanager
370    def JsonResponseModel(self):
371        """In this context, return raw JSON instead of proto."""
372        old_model = self.response_type_model
373        self.__response_type_model = 'json'
374        yield
375        self.__response_type_model = old_model
376
377    @property
378    def num_retries(self):
379        return self.__num_retries
380
381    @num_retries.setter
382    def num_retries(self, value):
383        util.Typecheck(value, six.integer_types)
384        if value < 0:
385            raise exceptions.InvalidDataError(
386                'Cannot have negative value for num_retries')
387        self.__num_retries = value
388
389    @property
390    def max_retry_wait(self):
391        return self.__max_retry_wait
392
393    @max_retry_wait.setter
394    def max_retry_wait(self, value):
395        util.Typecheck(value, six.integer_types)
396        if value <= 0:
397            raise exceptions.InvalidDataError(
398                'max_retry_wait must be a postiive integer')
399        self.__max_retry_wait = value
400
401    @contextlib.contextmanager
402    def WithRetries(self, num_retries):
403        old_num_retries = self.num_retries
404        self.num_retries = num_retries
405        yield
406        self.num_retries = old_num_retries
407
408    def ProcessRequest(self, method_config, request):
409        """Hook for pre-processing of requests."""
410        if self.log_request:
411            logging.info(
412                'Calling method %s with %s: %s', method_config.method_id,
413                method_config.request_type_name, request)
414        return request
415
416    def ProcessHttpRequest(self, http_request):
417        """Hook for pre-processing of http requests."""
418        http_request.headers.update(self.additional_http_headers)
419        if self.log_request:
420            logging.info('Making http %s to %s',
421                         http_request.http_method, http_request.url)
422            logging.info('Headers: %s', pprint.pformat(http_request.headers))
423            if http_request.body:
424                # TODO(craigcitro): Make this safe to print in the case of
425                # non-printable body characters.
426                logging.info('Body:\n%s',
427                             http_request.loggable_body or http_request.body)
428            else:
429                logging.info('Body: (none)')
430        return http_request
431
432    def ProcessResponse(self, method_config, response):
433        if self.log_response:
434            logging.info('Response of type %s: %s',
435                         method_config.response_type_name, response)
436        return response
437
438    # TODO(craigcitro): Decide where these two functions should live.
439    def SerializeMessage(self, message):
440        return encoding.MessageToJson(
441            message, include_fields=self.__include_fields)
442
443    def DeserializeMessage(self, response_type, data):
444        """Deserialize the given data as method_config.response_type."""
445        try:
446            message = encoding.JsonToMessage(response_type, data)
447        except (exceptions.InvalidDataFromServerError,
448                messages.ValidationError, ValueError) as e:
449            raise exceptions.InvalidDataFromServerError(
450                'Error decoding response "%s" as type %s: %s' % (
451                    data, response_type.__name__, e))
452        return message
453
454    def FinalizeTransferUrl(self, url):
455        """Modify the url for a given transfer, based on auth and version."""
456        url_builder = _UrlBuilder.FromUrl(url)
457        if self.global_params.key:
458            url_builder.query_params['key'] = self.global_params.key
459        return url_builder.url
460
461
462class BaseApiService(object):
463
464    """Base class for generated API services."""
465
466    def __init__(self, client):
467        self.__client = client
468        self._method_configs = {}
469        self._upload_configs = {}
470
471    @property
472    def _client(self):
473        return self.__client
474
475    @property
476    def client(self):
477        return self.__client
478
479    def GetMethodConfig(self, method):
480        """Returns service cached method config for given method."""
481        method_config = self._method_configs.get(method)
482        if method_config:
483            return method_config
484        func = getattr(self, method, None)
485        if func is None:
486            raise KeyError(method)
487        method_config = getattr(func, 'method_config', None)
488        if method_config is None:
489            raise KeyError(method)
490        self._method_configs[method] = config = method_config()
491        return config
492
493    @classmethod
494    def GetMethodsList(cls):
495        return [f.__name__ for f in six.itervalues(cls.__dict__)
496                if getattr(f, 'method_config', None)]
497
498    def GetUploadConfig(self, method):
499        return self._upload_configs.get(method)
500
501    def GetRequestType(self, method):
502        method_config = self.GetMethodConfig(method)
503        return getattr(self.client.MESSAGES_MODULE,
504                       method_config.request_type_name)
505
506    def GetResponseType(self, method):
507        method_config = self.GetMethodConfig(method)
508        return getattr(self.client.MESSAGES_MODULE,
509                       method_config.response_type_name)
510
511    def __CombineGlobalParams(self, global_params, default_params):
512        """Combine the given params with the defaults."""
513        util.Typecheck(global_params, (type(None), self.__client.params_type))
514        result = self.__client.params_type()
515        global_params = global_params or self.__client.params_type()
516        for field in result.all_fields():
517            value = global_params.get_assigned_value(field.name)
518            if value is None:
519                value = default_params.get_assigned_value(field.name)
520            if value not in (None, [], ()):
521                setattr(result, field.name, value)
522        return result
523
524    def __EncodePrettyPrint(self, query_info):
525        # The prettyPrint flag needs custom encoding: it should be encoded
526        # as 0 if False, and ignored otherwise (True is the default).
527        if not query_info.pop('prettyPrint', True):
528            query_info['prettyPrint'] = 0
529        # The One Platform equivalent of prettyPrint is pp, which also needs
530        # custom encoding.
531        if not query_info.pop('pp', True):
532            query_info['pp'] = 0
533        return query_info
534
535    def __FinalUrlValue(self, value, field):
536        """Encode value for the URL, using field to skip encoding for bytes."""
537        if isinstance(field, messages.BytesField) and value is not None:
538            return base64.urlsafe_b64encode(value)
539        elif isinstance(value, six.text_type):
540            return value.encode('utf8')
541        elif isinstance(value, six.binary_type):
542            return value.decode('utf8')
543        elif isinstance(value, datetime.datetime):
544            return value.isoformat()
545        return value
546
547    def __ConstructQueryParams(self, query_params, request, global_params):
548        """Construct a dictionary of query parameters for this request."""
549        # First, handle the global params.
550        global_params = self.__CombineGlobalParams(
551            global_params, self.__client.global_params)
552        global_param_names = util.MapParamNames(
553            [x.name for x in self.__client.params_type.all_fields()],
554            self.__client.params_type)
555        global_params_type = type(global_params)
556        query_info = dict(
557            (param,
558             self.__FinalUrlValue(getattr(global_params, param),
559                                  getattr(global_params_type, param)))
560            for param in global_param_names)
561        # Next, add the query params.
562        query_param_names = util.MapParamNames(query_params, type(request))
563        request_type = type(request)
564        query_info.update(
565            (param,
566             self.__FinalUrlValue(getattr(request, param, None),
567                                  getattr(request_type, param)))
568            for param in query_param_names)
569        query_info = dict((k, v) for k, v in query_info.items()
570                          if v is not None)
571        query_info = self.__EncodePrettyPrint(query_info)
572        query_info = util.MapRequestParams(query_info, type(request))
573        return query_info
574
575    def __ConstructRelativePath(self, method_config, request,
576                                relative_path=None):
577        """Determine the relative path for request."""
578        python_param_names = util.MapParamNames(
579            method_config.path_params, type(request))
580        params = dict([(param, getattr(request, param, None))
581                       for param in python_param_names])
582        params = util.MapRequestParams(params, type(request))
583        return util.ExpandRelativePath(method_config, params,
584                                       relative_path=relative_path)
585
586    def __FinalizeRequest(self, http_request, url_builder):
587        """Make any final general adjustments to the request."""
588        if (http_request.http_method == 'GET' and
589                len(http_request.url) > _MAX_URL_LENGTH):
590            http_request.http_method = 'POST'
591            http_request.headers['x-http-method-override'] = 'GET'
592            http_request.headers[
593                'content-type'] = 'application/x-www-form-urlencoded'
594            http_request.body = url_builder.query
595            url_builder.query_params = {}
596        http_request.url = url_builder.url
597
598    def __ProcessHttpResponse(self, method_config, http_response, request):
599        """Process the given http response."""
600        if http_response.status_code not in (http_client.OK,
601                                             http_client.CREATED,
602                                             http_client.NO_CONTENT):
603            raise exceptions.HttpError.FromResponse(
604                http_response, method_config=method_config, request=request)
605        if http_response.status_code == http_client.NO_CONTENT:
606            # TODO(craigcitro): Find out why _replace doesn't seem to work
607            # here.
608            http_response = http_wrapper.Response(
609                info=http_response.info, content='{}',
610                request_url=http_response.request_url)
611
612        content = http_response.content
613        if self._client.response_encoding and isinstance(content, bytes):
614            content = content.decode(self._client.response_encoding)
615
616        if self.__client.response_type_model == 'json':
617            return content
618        response_type = _LoadClass(method_config.response_type_name,
619                                   self.__client.MESSAGES_MODULE)
620        return self.__client.DeserializeMessage(response_type, content)
621
622    def __SetBaseHeaders(self, http_request, client):
623        """Fill in the basic headers on http_request."""
624        # TODO(craigcitro): Make the default a little better here, and
625        # include the apitools version.
626        user_agent = client.user_agent or 'apitools-client/1.0'
627        http_request.headers['user-agent'] = user_agent
628        http_request.headers['accept'] = 'application/json'
629        http_request.headers['accept-encoding'] = 'gzip, deflate'
630
631    def __SetBody(self, http_request, method_config, request, upload):
632        """Fill in the body on http_request."""
633        if not method_config.request_field:
634            return
635
636        request_type = _LoadClass(
637            method_config.request_type_name, self.__client.MESSAGES_MODULE)
638        if method_config.request_field == REQUEST_IS_BODY:
639            body_value = request
640            body_type = request_type
641        else:
642            body_value = getattr(request, method_config.request_field)
643            body_field = request_type.field_by_name(
644                method_config.request_field)
645            util.Typecheck(body_field, messages.MessageField)
646            body_type = body_field.type
647
648        # If there was no body provided, we use an empty message of the
649        # appropriate type.
650        body_value = body_value or body_type()
651        if upload and not body_value:
652            # We're going to fill in the body later.
653            return
654        util.Typecheck(body_value, body_type)
655        http_request.headers['content-type'] = 'application/json'
656        http_request.body = self.__client.SerializeMessage(body_value)
657
658    def PrepareHttpRequest(self, method_config, request, global_params=None,
659                           upload=None, upload_config=None, download=None):
660        """Prepares an HTTP request to be sent."""
661        request_type = _LoadClass(
662            method_config.request_type_name, self.__client.MESSAGES_MODULE)
663        util.Typecheck(request, request_type)
664        request = self.__client.ProcessRequest(method_config, request)
665
666        http_request = http_wrapper.Request(
667            http_method=method_config.http_method)
668        self.__SetBaseHeaders(http_request, self.__client)
669        self.__SetBody(http_request, method_config, request, upload)
670
671        url_builder = _UrlBuilder(
672            self.__client.url, relative_path=method_config.relative_path)
673        url_builder.query_params = self.__ConstructQueryParams(
674            method_config.query_params, request, global_params)
675
676        # It's important that upload and download go before we fill in the
677        # relative path, so that they can replace it.
678        if upload is not None:
679            upload.ConfigureRequest(upload_config, http_request, url_builder)
680        if download is not None:
681            download.ConfigureRequest(http_request, url_builder)
682
683        url_builder.relative_path = self.__ConstructRelativePath(
684            method_config, request, relative_path=url_builder.relative_path)
685        self.__FinalizeRequest(http_request, url_builder)
686
687        return self.__client.ProcessHttpRequest(http_request)
688
689    def _RunMethod(self, method_config, request, global_params=None,
690                   upload=None, upload_config=None, download=None):
691        """Call this method with request."""
692        if upload is not None and download is not None:
693            # TODO(craigcitro): This just involves refactoring the logic
694            # below into callbacks that we can pass around; in particular,
695            # the order should be that the upload gets the initial request,
696            # and then passes its reply to a download if one exists, and
697            # then that goes to ProcessResponse and is returned.
698            raise exceptions.NotYetImplementedError(
699                'Cannot yet use both upload and download at once')
700
701        http_request = self.PrepareHttpRequest(
702            method_config, request, global_params, upload, upload_config,
703            download)
704
705        # TODO(craigcitro): Make num_retries customizable on Transfer
706        # objects, and pass in self.__client.num_retries when initializing
707        # an upload or download.
708        if download is not None:
709            download.InitializeDownload(http_request, client=self.client)
710            return
711
712        http_response = None
713        if upload is not None:
714            http_response = upload.InitializeUpload(
715                http_request, client=self.client)
716        if http_response is None:
717            http = self.__client.http
718            if upload and upload.bytes_http:
719                http = upload.bytes_http
720            opts = {
721                'retries': self.__client.num_retries,
722                'max_retry_wait': self.__client.max_retry_wait,
723            }
724            if self.__client.check_response_func:
725                opts['check_response_func'] = self.__client.check_response_func
726            if self.__client.retry_func:
727                opts['retry_func'] = self.__client.retry_func
728            http_response = http_wrapper.MakeRequest(
729                http, http_request, **opts)
730
731        return self.ProcessHttpResponse(method_config, http_response, request)
732
733    def ProcessHttpResponse(self, method_config, http_response, request=None):
734        """Convert an HTTP response to the expected message type."""
735        return self.__client.ProcessResponse(
736            method_config,
737            self.__ProcessHttpResponse(method_config, http_response, request))
738