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