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