OLD | NEW |
(Empty) | |
| 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 |
| 19 import base64 |
| 20 import contextlib |
| 21 import datetime |
| 22 import logging |
| 23 import pprint |
| 24 |
| 25 |
| 26 import six |
| 27 from six.moves import http_client |
| 28 from six.moves import urllib |
| 29 |
| 30 |
| 31 from apitools.base.protorpclite import message_types |
| 32 from apitools.base.protorpclite import messages |
| 33 from apitools.base.py import encoding |
| 34 from apitools.base.py import exceptions |
| 35 from apitools.base.py import http_wrapper |
| 36 from 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). |
| 48 logging.getLogger('oauth2client.util').setLevel(logging.ERROR) |
| 49 |
| 50 _MAX_URL_LENGTH = 2048 |
| 51 |
| 52 |
| 53 class 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 |
| 77 class 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 method_id: ID for this method. |
| 86 http_method: HTTP verb to use for this method. |
| 87 path_params: (repeated) path parameters for this method. |
| 88 query_params: (repeated) query parameters for this method. |
| 89 ordered_params: (repeated) ordered list of parameters for |
| 90 this method. |
| 91 description: description of this method. |
| 92 request_type_name: name of the request type. |
| 93 response_type_name: name of the response type. |
| 94 request_field: if not null, the field to pass as the body |
| 95 of this POST request. may also be the REQUEST_IS_BODY |
| 96 value below to indicate the whole message is the body. |
| 97 upload_config: (ApiUploadInfo) Information about the upload |
| 98 configuration supported by this method. |
| 99 supports_download: (boolean) If True, this method supports |
| 100 downloading the request via the `alt=media` query |
| 101 parameter. |
| 102 """ |
| 103 |
| 104 relative_path = messages.StringField(1) |
| 105 method_id = messages.StringField(2) |
| 106 http_method = messages.StringField(3) |
| 107 path_params = messages.StringField(4, repeated=True) |
| 108 query_params = messages.StringField(5, repeated=True) |
| 109 ordered_params = messages.StringField(6, repeated=True) |
| 110 description = messages.StringField(7) |
| 111 request_type_name = messages.StringField(8) |
| 112 response_type_name = messages.StringField(9) |
| 113 request_field = messages.StringField(10, default='') |
| 114 upload_config = messages.MessageField(ApiUploadInfo, 11) |
| 115 supports_download = messages.BooleanField(12, default=False) |
| 116 REQUEST_IS_BODY = '<request>' |
| 117 |
| 118 |
| 119 def _LoadClass(name, messages_module): |
| 120 if name.startswith('message_types.'): |
| 121 _, _, classname = name.partition('.') |
| 122 return getattr(message_types, classname) |
| 123 elif '.' not in name: |
| 124 return getattr(messages_module, name) |
| 125 else: |
| 126 raise exceptions.GeneratedClientError('Unknown class %s' % name) |
| 127 |
| 128 |
| 129 def _RequireClassAttrs(obj, attrs): |
| 130 for attr in attrs: |
| 131 attr_name = attr.upper() |
| 132 if not hasattr(obj, '%s' % attr_name) or not getattr(obj, attr_name): |
| 133 msg = 'No %s specified for object of class %s.' % ( |
| 134 attr_name, type(obj).__name__) |
| 135 raise exceptions.GeneratedClientError(msg) |
| 136 |
| 137 |
| 138 def NormalizeApiEndpoint(api_endpoint): |
| 139 if not api_endpoint.endswith('/'): |
| 140 api_endpoint += '/' |
| 141 return api_endpoint |
| 142 |
| 143 |
| 144 def _urljoin(base, url): # pylint: disable=invalid-name |
| 145 """Custom urljoin replacement supporting : before / in url.""" |
| 146 # In general, it's unsafe to simply join base and url. However, for |
| 147 # the case of discovery documents, we know: |
| 148 # * base will never contain params, query, or fragment |
| 149 # * url will never contain a scheme or net_loc. |
| 150 # In general, this means we can safely join on /; we just need to |
| 151 # ensure we end up with precisely one / joining base and url. The |
| 152 # exception here is the case of media uploads, where url will be an |
| 153 # absolute url. |
| 154 if url.startswith('http://') or url.startswith('https://'): |
| 155 return urllib.parse.urljoin(base, url) |
| 156 new_base = base if base.endswith('/') else base + '/' |
| 157 new_url = url[1:] if url.startswith('/') else url |
| 158 return new_base + new_url |
| 159 |
| 160 |
| 161 class _UrlBuilder(object): |
| 162 |
| 163 """Convenient container for url data.""" |
| 164 |
| 165 def __init__(self, base_url, relative_path=None, query_params=None): |
| 166 components = urllib.parse.urlsplit(_urljoin( |
| 167 base_url, relative_path or '')) |
| 168 if components.fragment: |
| 169 raise exceptions.ConfigurationValueError( |
| 170 'Unexpected url fragment: %s' % components.fragment) |
| 171 self.query_params = urllib.parse.parse_qs(components.query or '') |
| 172 if query_params is not None: |
| 173 self.query_params.update(query_params) |
| 174 self.__scheme = components.scheme |
| 175 self.__netloc = components.netloc |
| 176 self.relative_path = components.path or '' |
| 177 |
| 178 @classmethod |
| 179 def FromUrl(cls, url): |
| 180 urlparts = urllib.parse.urlsplit(url) |
| 181 query_params = urllib.parse.parse_qs(urlparts.query) |
| 182 base_url = urllib.parse.urlunsplit(( |
| 183 urlparts.scheme, urlparts.netloc, '', None, None)) |
| 184 relative_path = urlparts.path or '' |
| 185 return cls( |
| 186 base_url, relative_path=relative_path, query_params=query_params) |
| 187 |
| 188 @property |
| 189 def base_url(self): |
| 190 return urllib.parse.urlunsplit( |
| 191 (self.__scheme, self.__netloc, '', '', '')) |
| 192 |
| 193 @base_url.setter |
| 194 def base_url(self, value): |
| 195 components = urllib.parse.urlsplit(value) |
| 196 if components.path or components.query or components.fragment: |
| 197 raise exceptions.ConfigurationValueError( |
| 198 'Invalid base url: %s' % value) |
| 199 self.__scheme = components.scheme |
| 200 self.__netloc = components.netloc |
| 201 |
| 202 @property |
| 203 def query(self): |
| 204 # TODO(craigcitro): In the case that some of the query params are |
| 205 # non-ASCII, we may silently fail to encode correctly. We should |
| 206 # figure out who is responsible for owning the object -> str |
| 207 # conversion. |
| 208 return urllib.parse.urlencode(self.query_params, True) |
| 209 |
| 210 @property |
| 211 def url(self): |
| 212 if '{' in self.relative_path or '}' in self.relative_path: |
| 213 raise exceptions.ConfigurationValueError( |
| 214 'Cannot create url with relative path %s' % self.relative_path) |
| 215 return urllib.parse.urlunsplit(( |
| 216 self.__scheme, self.__netloc, self.relative_path, self.query, '')) |
| 217 |
| 218 |
| 219 def _SkipGetCredentials(): |
| 220 """Hook for skipping credentials. For internal use.""" |
| 221 return False |
| 222 |
| 223 |
| 224 class BaseApiClient(object): |
| 225 |
| 226 """Base class for client libraries.""" |
| 227 MESSAGES_MODULE = None |
| 228 |
| 229 _API_KEY = '' |
| 230 _CLIENT_ID = '' |
| 231 _CLIENT_SECRET = '' |
| 232 _PACKAGE = '' |
| 233 _SCOPES = [] |
| 234 _USER_AGENT = '' |
| 235 |
| 236 def __init__(self, url, credentials=None, get_credentials=True, http=None, |
| 237 model=None, log_request=False, log_response=False, |
| 238 num_retries=5, max_retry_wait=60, credentials_args=None, |
| 239 default_global_params=None, additional_http_headers=None, |
| 240 check_response_func=None): |
| 241 _RequireClassAttrs(self, ('_package', '_scopes', 'messages_module')) |
| 242 if default_global_params is not None: |
| 243 util.Typecheck(default_global_params, self.params_type) |
| 244 self.__default_global_params = default_global_params |
| 245 self.log_request = log_request |
| 246 self.log_response = log_response |
| 247 self.__num_retries = 5 |
| 248 self.__max_retry_wait = 60 |
| 249 # We let the @property machinery below do our validation. |
| 250 self.num_retries = num_retries |
| 251 self.max_retry_wait = max_retry_wait |
| 252 self._credentials = credentials |
| 253 get_credentials = get_credentials and not _SkipGetCredentials() |
| 254 if get_credentials and not credentials: |
| 255 credentials_args = credentials_args or {} |
| 256 self._SetCredentials(**credentials_args) |
| 257 self._url = NormalizeApiEndpoint(url) |
| 258 self._http = http or http_wrapper.GetHttp() |
| 259 # Note that "no credentials" is totally possible. |
| 260 if self._credentials is not None: |
| 261 self._http = self._credentials.authorize(self._http) |
| 262 # TODO(craigcitro): Remove this field when we switch to proto2. |
| 263 self.__include_fields = None |
| 264 |
| 265 self.additional_http_headers = additional_http_headers or {} |
| 266 self.__check_response_func = check_response_func |
| 267 |
| 268 # TODO(craigcitro): Finish deprecating these fields. |
| 269 _ = model |
| 270 |
| 271 self.__response_type_model = 'proto' |
| 272 |
| 273 def _SetCredentials(self, **kwds): |
| 274 """Fetch credentials, and set them for this client. |
| 275 |
| 276 Note that we can't simply return credentials, since creating them |
| 277 may involve side-effecting self. |
| 278 |
| 279 Args: |
| 280 **kwds: Additional keyword arguments are passed on to GetCredentials. |
| 281 |
| 282 Returns: |
| 283 None. Sets self._credentials. |
| 284 """ |
| 285 args = { |
| 286 'api_key': self._API_KEY, |
| 287 'client': self, |
| 288 'client_id': self._CLIENT_ID, |
| 289 'client_secret': self._CLIENT_SECRET, |
| 290 'package_name': self._PACKAGE, |
| 291 'scopes': self._SCOPES, |
| 292 'user_agent': self._USER_AGENT, |
| 293 } |
| 294 args.update(kwds) |
| 295 # credentials_lib can be expensive to import so do it only if needed. |
| 296 from apitools.base.py import credentials_lib |
| 297 # TODO(craigcitro): It's a bit dangerous to pass this |
| 298 # still-half-initialized self into this method, but we might need |
| 299 # to set attributes on it associated with our credentials. |
| 300 # Consider another way around this (maybe a callback?) and whether |
| 301 # or not it's worth it. |
| 302 self._credentials = credentials_lib.GetCredentials(**args) |
| 303 |
| 304 @classmethod |
| 305 def ClientInfo(cls): |
| 306 return { |
| 307 'client_id': cls._CLIENT_ID, |
| 308 'client_secret': cls._CLIENT_SECRET, |
| 309 'scope': ' '.join(sorted(util.NormalizeScopes(cls._SCOPES))), |
| 310 'user_agent': cls._USER_AGENT, |
| 311 } |
| 312 |
| 313 @property |
| 314 def base_model_class(self): |
| 315 return None |
| 316 |
| 317 @property |
| 318 def http(self): |
| 319 return self._http |
| 320 |
| 321 @property |
| 322 def url(self): |
| 323 return self._url |
| 324 |
| 325 @classmethod |
| 326 def GetScopes(cls): |
| 327 return cls._SCOPES |
| 328 |
| 329 @property |
| 330 def params_type(self): |
| 331 return _LoadClass('StandardQueryParameters', self.MESSAGES_MODULE) |
| 332 |
| 333 @property |
| 334 def user_agent(self): |
| 335 return self._USER_AGENT |
| 336 |
| 337 @property |
| 338 def _default_global_params(self): |
| 339 if self.__default_global_params is None: |
| 340 self.__default_global_params = self.params_type() |
| 341 return self.__default_global_params |
| 342 |
| 343 def AddGlobalParam(self, name, value): |
| 344 params = self._default_global_params |
| 345 setattr(params, name, value) |
| 346 |
| 347 @property |
| 348 def global_params(self): |
| 349 return encoding.CopyProtoMessage(self._default_global_params) |
| 350 |
| 351 @contextlib.contextmanager |
| 352 def IncludeFields(self, include_fields): |
| 353 self.__include_fields = include_fields |
| 354 yield |
| 355 self.__include_fields = None |
| 356 |
| 357 @property |
| 358 def response_type_model(self): |
| 359 return self.__response_type_model |
| 360 |
| 361 @contextlib.contextmanager |
| 362 def JsonResponseModel(self): |
| 363 """In this context, return raw JSON instead of proto.""" |
| 364 old_model = self.response_type_model |
| 365 self.__response_type_model = 'json' |
| 366 yield |
| 367 self.__response_type_model = old_model |
| 368 |
| 369 @property |
| 370 def num_retries(self): |
| 371 return self.__num_retries |
| 372 |
| 373 @num_retries.setter |
| 374 def num_retries(self, value): |
| 375 util.Typecheck(value, six.integer_types) |
| 376 if value < 0: |
| 377 raise exceptions.InvalidDataError( |
| 378 'Cannot have negative value for num_retries') |
| 379 self.__num_retries = value |
| 380 |
| 381 @property |
| 382 def check_response_func(self): |
| 383 return self.__check_response_func |
| 384 |
| 385 @check_response_func.setter |
| 386 def check_response_func(self, value): |
| 387 self.__check_response_func = 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 |
| 462 class 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 return self._method_configs[method] |
| 481 |
| 482 def GetUploadConfig(self, method): |
| 483 return self._upload_configs.get(method) |
| 484 |
| 485 def GetRequestType(self, method): |
| 486 method_config = self.GetMethodConfig(method) |
| 487 return getattr(self.client.MESSAGES_MODULE, |
| 488 method_config.request_type_name) |
| 489 |
| 490 def GetResponseType(self, method): |
| 491 method_config = self.GetMethodConfig(method) |
| 492 return getattr(self.client.MESSAGES_MODULE, |
| 493 method_config.response_type_name) |
| 494 |
| 495 def __CombineGlobalParams(self, global_params, default_params): |
| 496 """Combine the given params with the defaults.""" |
| 497 util.Typecheck(global_params, (type(None), self.__client.params_type)) |
| 498 result = self.__client.params_type() |
| 499 global_params = global_params or self.__client.params_type() |
| 500 for field in result.all_fields(): |
| 501 value = global_params.get_assigned_value(field.name) |
| 502 if value is None: |
| 503 value = default_params.get_assigned_value(field.name) |
| 504 if value not in (None, [], ()): |
| 505 setattr(result, field.name, value) |
| 506 return result |
| 507 |
| 508 def __EncodePrettyPrint(self, query_info): |
| 509 # The prettyPrint flag needs custom encoding: it should be encoded |
| 510 # as 0 if False, and ignored otherwise (True is the default). |
| 511 if not query_info.pop('prettyPrint', True): |
| 512 query_info['prettyPrint'] = 0 |
| 513 # The One Platform equivalent of prettyPrint is pp, which also needs |
| 514 # custom encoding. |
| 515 if not query_info.pop('pp', True): |
| 516 query_info['pp'] = 0 |
| 517 return query_info |
| 518 |
| 519 def __FinalUrlValue(self, value, field): |
| 520 """Encode value for the URL, using field to skip encoding for bytes.""" |
| 521 if isinstance(field, messages.BytesField) and value is not None: |
| 522 return base64.urlsafe_b64encode(value) |
| 523 elif isinstance(value, six.text_type): |
| 524 return value.encode('utf8') |
| 525 elif isinstance(value, six.binary_type): |
| 526 return value.decode('utf8') |
| 527 elif isinstance(value, datetime.datetime): |
| 528 return value.isoformat() |
| 529 return value |
| 530 |
| 531 def __ConstructQueryParams(self, query_params, request, global_params): |
| 532 """Construct a dictionary of query parameters for this request.""" |
| 533 # First, handle the global params. |
| 534 global_params = self.__CombineGlobalParams( |
| 535 global_params, self.__client.global_params) |
| 536 global_param_names = util.MapParamNames( |
| 537 [x.name for x in self.__client.params_type.all_fields()], |
| 538 self.__client.params_type) |
| 539 global_params_type = type(global_params) |
| 540 query_info = dict( |
| 541 (param, |
| 542 self.__FinalUrlValue(getattr(global_params, param), |
| 543 getattr(global_params_type, param))) |
| 544 for param in global_param_names) |
| 545 # Next, add the query params. |
| 546 query_param_names = util.MapParamNames(query_params, type(request)) |
| 547 request_type = type(request) |
| 548 query_info.update( |
| 549 (param, |
| 550 self.__FinalUrlValue(getattr(request, param, None), |
| 551 getattr(request_type, param))) |
| 552 for param in query_param_names) |
| 553 query_info = dict((k, v) for k, v in query_info.items() |
| 554 if v is not None) |
| 555 query_info = self.__EncodePrettyPrint(query_info) |
| 556 query_info = util.MapRequestParams(query_info, type(request)) |
| 557 return query_info |
| 558 |
| 559 def __ConstructRelativePath(self, method_config, request, |
| 560 relative_path=None): |
| 561 """Determine the relative path for request.""" |
| 562 python_param_names = util.MapParamNames( |
| 563 method_config.path_params, type(request)) |
| 564 params = dict([(param, getattr(request, param, None)) |
| 565 for param in python_param_names]) |
| 566 params = util.MapRequestParams(params, type(request)) |
| 567 return util.ExpandRelativePath(method_config, params, |
| 568 relative_path=relative_path) |
| 569 |
| 570 def __FinalizeRequest(self, http_request, url_builder): |
| 571 """Make any final general adjustments to the request.""" |
| 572 if (http_request.http_method == 'GET' and |
| 573 len(http_request.url) > _MAX_URL_LENGTH): |
| 574 http_request.http_method = 'POST' |
| 575 http_request.headers['x-http-method-override'] = 'GET' |
| 576 http_request.headers[ |
| 577 'content-type'] = 'application/x-www-form-urlencoded' |
| 578 http_request.body = url_builder.query |
| 579 url_builder.query_params = {} |
| 580 http_request.url = url_builder.url |
| 581 |
| 582 def __ProcessHttpResponse(self, method_config, http_response): |
| 583 """Process the given http response.""" |
| 584 if http_response.status_code not in (http_client.OK, |
| 585 http_client.NO_CONTENT): |
| 586 raise exceptions.HttpError.FromResponse(http_response) |
| 587 if http_response.status_code == http_client.NO_CONTENT: |
| 588 # TODO(craigcitro): Find out why _replace doesn't seem to work |
| 589 # here. |
| 590 http_response = http_wrapper.Response( |
| 591 info=http_response.info, content='{}', |
| 592 request_url=http_response.request_url) |
| 593 if self.__client.response_type_model == 'json': |
| 594 return http_response.content |
| 595 else: |
| 596 response_type = _LoadClass(method_config.response_type_name, |
| 597 self.__client.MESSAGES_MODULE) |
| 598 return self.__client.DeserializeMessage( |
| 599 response_type, http_response.content) |
| 600 |
| 601 def __SetBaseHeaders(self, http_request, client): |
| 602 """Fill in the basic headers on http_request.""" |
| 603 # TODO(craigcitro): Make the default a little better here, and |
| 604 # include the apitools version. |
| 605 user_agent = client.user_agent or 'apitools-client/1.0' |
| 606 http_request.headers['user-agent'] = user_agent |
| 607 http_request.headers['accept'] = 'application/json' |
| 608 http_request.headers['accept-encoding'] = 'gzip, deflate' |
| 609 |
| 610 def __SetBody(self, http_request, method_config, request, upload): |
| 611 """Fill in the body on http_request.""" |
| 612 if not method_config.request_field: |
| 613 return |
| 614 |
| 615 request_type = _LoadClass( |
| 616 method_config.request_type_name, self.__client.MESSAGES_MODULE) |
| 617 if method_config.request_field == REQUEST_IS_BODY: |
| 618 body_value = request |
| 619 body_type = request_type |
| 620 else: |
| 621 body_value = getattr(request, method_config.request_field) |
| 622 body_field = request_type.field_by_name( |
| 623 method_config.request_field) |
| 624 util.Typecheck(body_field, messages.MessageField) |
| 625 body_type = body_field.type |
| 626 |
| 627 # If there was no body provided, we use an empty message of the |
| 628 # appropriate type. |
| 629 body_value = body_value or body_type() |
| 630 if upload and not body_value: |
| 631 # We're going to fill in the body later. |
| 632 return |
| 633 util.Typecheck(body_value, body_type) |
| 634 http_request.headers['content-type'] = 'application/json' |
| 635 http_request.body = self.__client.SerializeMessage(body_value) |
| 636 |
| 637 def PrepareHttpRequest(self, method_config, request, global_params=None, |
| 638 upload=None, upload_config=None, download=None): |
| 639 """Prepares an HTTP request to be sent.""" |
| 640 request_type = _LoadClass( |
| 641 method_config.request_type_name, self.__client.MESSAGES_MODULE) |
| 642 util.Typecheck(request, request_type) |
| 643 request = self.__client.ProcessRequest(method_config, request) |
| 644 |
| 645 http_request = http_wrapper.Request( |
| 646 http_method=method_config.http_method) |
| 647 self.__SetBaseHeaders(http_request, self.__client) |
| 648 self.__SetBody(http_request, method_config, request, upload) |
| 649 |
| 650 url_builder = _UrlBuilder( |
| 651 self.__client.url, relative_path=method_config.relative_path) |
| 652 url_builder.query_params = self.__ConstructQueryParams( |
| 653 method_config.query_params, request, global_params) |
| 654 |
| 655 # It's important that upload and download go before we fill in the |
| 656 # relative path, so that they can replace it. |
| 657 if upload is not None: |
| 658 upload.ConfigureRequest(upload_config, http_request, url_builder) |
| 659 if download is not None: |
| 660 download.ConfigureRequest(http_request, url_builder) |
| 661 |
| 662 url_builder.relative_path = self.__ConstructRelativePath( |
| 663 method_config, request, relative_path=url_builder.relative_path) |
| 664 self.__FinalizeRequest(http_request, url_builder) |
| 665 |
| 666 return self.__client.ProcessHttpRequest(http_request) |
| 667 |
| 668 def _RunMethod(self, method_config, request, global_params=None, |
| 669 upload=None, upload_config=None, download=None): |
| 670 """Call this method with request.""" |
| 671 if upload is not None and download is not None: |
| 672 # TODO(craigcitro): This just involves refactoring the logic |
| 673 # below into callbacks that we can pass around; in particular, |
| 674 # the order should be that the upload gets the initial request, |
| 675 # and then passes its reply to a download if one exists, and |
| 676 # then that goes to ProcessResponse and is returned. |
| 677 raise exceptions.NotYetImplementedError( |
| 678 'Cannot yet use both upload and download at once') |
| 679 |
| 680 http_request = self.PrepareHttpRequest( |
| 681 method_config, request, global_params, upload, upload_config, |
| 682 download) |
| 683 |
| 684 # TODO(craigcitro): Make num_retries customizable on Transfer |
| 685 # objects, and pass in self.__client.num_retries when initializing |
| 686 # an upload or download. |
| 687 if download is not None: |
| 688 download.InitializeDownload(http_request, client=self.client) |
| 689 return |
| 690 |
| 691 http_response = None |
| 692 if upload is not None: |
| 693 http_response = upload.InitializeUpload( |
| 694 http_request, client=self.client) |
| 695 if http_response is None: |
| 696 http = self.__client.http |
| 697 if upload and upload.bytes_http: |
| 698 http = upload.bytes_http |
| 699 opts = { |
| 700 'retries': self.__client.num_retries, |
| 701 'max_retry_wait': self.__client.max_retry_wait, |
| 702 } |
| 703 if self.__client.check_response_func: |
| 704 opts['check_response_func'] = self.__client.check_response_func |
| 705 http_response = http_wrapper.MakeRequest( |
| 706 http, http_request, **opts) |
| 707 |
| 708 return self.ProcessHttpResponse(method_config, http_response) |
| 709 |
| 710 def ProcessHttpResponse(self, method_config, http_response): |
| 711 """Convert an HTTP response to the expected message type.""" |
| 712 return self.__client.ProcessResponse( |
| 713 method_config, |
| 714 self.__ProcessHttpResponse(method_config, http_response)) |
OLD | NEW |