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