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