ibm-cloud-sdk-core 3.16.0__py3-none-any.whl → 3.21.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. ibm_cloud_sdk_core/__init__.py +1 -0
  2. ibm_cloud_sdk_core/api_exception.py +18 -4
  3. ibm_cloud_sdk_core/authenticators/__init__.py +1 -0
  4. ibm_cloud_sdk_core/authenticators/authenticator.py +2 -1
  5. ibm_cloud_sdk_core/authenticators/basic_authenticator.py +12 -7
  6. ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py +7 -2
  7. ibm_cloud_sdk_core/authenticators/container_authenticator.py +25 -16
  8. ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py +38 -23
  9. ibm_cloud_sdk_core/authenticators/iam_authenticator.py +22 -13
  10. ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py +10 -8
  11. ibm_cloud_sdk_core/authenticators/mcsp_authenticator.py +134 -0
  12. ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py +12 -11
  13. ibm_cloud_sdk_core/base_service.py +137 -103
  14. ibm_cloud_sdk_core/detailed_response.py +21 -15
  15. ibm_cloud_sdk_core/get_authenticator.py +35 -17
  16. ibm_cloud_sdk_core/http_adapter.py +28 -0
  17. ibm_cloud_sdk_core/logger.py +85 -0
  18. ibm_cloud_sdk_core/private_helpers.py +34 -0
  19. ibm_cloud_sdk_core/token_managers/container_token_manager.py +63 -33
  20. ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py +35 -22
  21. ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py +50 -21
  22. ibm_cloud_sdk_core/token_managers/iam_token_manager.py +24 -13
  23. ibm_cloud_sdk_core/token_managers/jwt_token_manager.py +3 -16
  24. ibm_cloud_sdk_core/token_managers/mcsp_token_manager.py +108 -0
  25. ibm_cloud_sdk_core/token_managers/token_manager.py +20 -23
  26. ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py +37 -18
  27. ibm_cloud_sdk_core/utils.py +127 -48
  28. ibm_cloud_sdk_core/version.py +1 -1
  29. ibm_cloud_sdk_core-3.21.0.dist-info/METADATA +159 -0
  30. ibm_cloud_sdk_core-3.21.0.dist-info/RECORD +35 -0
  31. {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.21.0.dist-info}/WHEEL +1 -1
  32. ibm_cloud_sdk_core-3.21.0.dist-info/top_level.txt +1 -0
  33. ibm_cloud_sdk_core-3.16.0.dist-info/METADATA +0 -112
  34. ibm_cloud_sdk_core-3.16.0.dist-info/RECORD +0 -52
  35. ibm_cloud_sdk_core-3.16.0.dist-info/top_level.txt +0 -3
  36. ibm_cloud_sdk_core-3.16.0.dist-info/zip-safe +0 -1
  37. test/__init__.py +0 -0
  38. test/test_api_exception.py +0 -73
  39. test/test_authenticator.py +0 -21
  40. test/test_base_service.py +0 -933
  41. test/test_basic_authenticator.py +0 -36
  42. test/test_bearer_authenticator.py +0 -28
  43. test/test_container_authenticator.py +0 -105
  44. test/test_container_token_manager.py +0 -283
  45. test/test_cp4d_authenticator.py +0 -171
  46. test/test_cp4d_token_manager.py +0 -56
  47. test/test_detailed_response.py +0 -57
  48. test/test_iam_authenticator.py +0 -157
  49. test/test_iam_token_manager.py +0 -362
  50. test/test_jwt_token_manager.py +0 -109
  51. test/test_no_auth_authenticator.py +0 -15
  52. test/test_token_manager.py +0 -84
  53. test/test_utils.py +0 -634
  54. test/test_vpc_instance_authenticator.py +0 -66
  55. test/test_vpc_instance_token_manager.py +0 -266
  56. test_integration/__init__.py +0 -0
  57. test_integration/test_cp4d_authenticator_integration.py +0 -45
  58. {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.21.0.dist-info}/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2021 IBM All Rights Reserved.
3
+ # Copyright 2021, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -18,9 +18,12 @@ from typing import Optional
18
18
 
19
19
  from requests import Request
20
20
 
21
+ from ibm_cloud_sdk_core.logger import get_logger
21
22
  from ..token_managers.vpc_instance_token_manager import VPCInstanceTokenManager
22
23
  from .authenticator import Authenticator
23
24
 
25
+ logger = get_logger()
26
+
24
27
 
25
28
  class VPCInstanceAuthenticator(Authenticator):
26
29
  """VPCInstanceAuthenticator implements an authentication scheme in which it
@@ -53,16 +56,15 @@ class VPCInstanceAuthenticator(Authenticator):
53
56
 
54
57
  DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254'
55
58
 
56
- def __init__(self,
57
- iam_profile_crn: Optional[str] = None,
58
- iam_profile_id: Optional[str] = None,
59
- url: Optional[str] = None) -> None:
60
-
59
+ def __init__(
60
+ self, iam_profile_crn: Optional[str] = None, iam_profile_id: Optional[str] = None, url: Optional[str] = None
61
+ ) -> None:
61
62
  if not url:
62
63
  url = self.DEFAULT_IMS_ENDPOINT
63
64
 
64
65
  self.token_manager = VPCInstanceTokenManager(
65
- url=url, iam_profile_crn=iam_profile_crn, iam_profile_id=iam_profile_id)
66
+ url=url, iam_profile_crn=iam_profile_crn, iam_profile_id=iam_profile_id
67
+ )
66
68
 
67
69
  self.validate()
68
70
 
@@ -74,8 +76,7 @@ class VPCInstanceAuthenticator(Authenticator):
74
76
  super().validate()
75
77
 
76
78
  if self.token_manager.iam_profile_crn and self.token_manager.iam_profile_id:
77
- raise ValueError(
78
- 'At most one of "iam_profile_id" or "iam_profile_crn" may be specified.')
79
+ raise ValueError('At most one of "iam_profile_id" or "iam_profile_crn" may be specified.')
79
80
 
80
81
  def authenticate(self, req: Request) -> None:
81
82
  """Adds IAM authentication information to the request.
@@ -85,13 +86,13 @@ class VPCInstanceAuthenticator(Authenticator):
85
86
  Authorization: Bearer <bearer-token>
86
87
 
87
88
  Args:
88
- req: The request to add IAM authentication information too. Must contain a key to a dictionary
89
+ req: The request to add IAM authentication information to. Must contain a key to a dictionary
89
90
  called headers.
90
91
  """
91
92
  headers = req.get('headers')
92
93
  bearer_token = self.token_manager.get_token()
93
94
  headers['Authorization'] = 'Bearer {0}'.format(bearer_token)
94
-
95
+ logger.debug('Authenticated outbound request (type=%s)', self.authentication_type())
95
96
 
96
97
  def set_iam_profile_crn(self, iam_profile_crn: str) -> None:
97
98
  """Sets CRN of the IAM profile.
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
 
3
- # Copyright 2019 IBM All Rights Reserved.
3
+ # Copyright 2019, 2024 IBM All Rights Reserved.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -15,36 +15,44 @@
15
15
  # limitations under the License.
16
16
 
17
17
  import gzip
18
- import json as json_import
18
+ import io
19
19
  import logging
20
- import platform
20
+ import json as json_import
21
21
  from http.cookiejar import CookieJar
22
+ from http import client
22
23
  from os.path import basename
23
24
  from typing import Dict, List, Optional, Tuple, Union
24
25
  from urllib3.util.retry import Retry
25
26
 
26
27
  import requests
27
28
  from requests.structures import CaseInsensitiveDict
29
+ from requests.exceptions import JSONDecodeError
28
30
 
29
31
  from ibm_cloud_sdk_core.authenticators import Authenticator
30
32
  from .api_exception import ApiException
31
33
  from .detailed_response import DetailedResponse
34
+ from .http_adapter import SSLHTTPAdapter
32
35
  from .token_managers.token_manager import TokenManager
33
- from .utils import (has_bad_first_or_last_char, remove_null_values,
34
- cleanup_values, read_external_sources, strip_extra_slashes,
35
- SSLHTTPAdapter)
36
- from .version import __version__
37
-
38
- # Uncomment this to enable http debugging
39
- # import http.client as http_client
40
- # http_client.HTTPConnection.debuglevel = 1
41
-
42
-
43
- logger = logging.getLogger(__name__)
44
-
45
-
46
- #pylint: disable=too-many-instance-attributes
47
- #pylint: disable=too-many-locals
36
+ from .utils import (
37
+ has_bad_first_or_last_char,
38
+ is_json_mimetype,
39
+ remove_null_values,
40
+ cleanup_values,
41
+ read_external_sources,
42
+ strip_extra_slashes,
43
+ GzipStream,
44
+ )
45
+ from .private_helpers import _build_user_agent
46
+ from .logger import (
47
+ get_logger,
48
+ LoggingFilter,
49
+ )
50
+
51
+ logger = get_logger()
52
+
53
+
54
+ # pylint: disable=too-many-instance-attributes
55
+ # pylint: disable=too-many-locals
48
56
  class BaseService:
49
57
  """Common functionality shared by generated service classes.
50
58
 
@@ -72,18 +80,22 @@ class BaseService:
72
80
  Raises:
73
81
  ValueError: If Authenticator is not provided or invalid type.
74
82
  """
75
- SDK_NAME = 'ibm-python-sdk-core'
76
- ERROR_MSG_DISABLE_SSL = 'The connection failed because the SSL certificate is not valid. To use a self-signed '\
77
- 'certificate, disable verification of the server\'s SSL certificate by invoking the '\
78
- 'set_disable_ssl_verification(True) on your service instance and/ or use the '\
79
- 'disable_ssl_verification option of the authenticator.'
80
-
81
- def __init__(self,
82
- *,
83
- service_url: str = None,
84
- authenticator: Authenticator = None,
85
- disable_ssl_verification: bool = False,
86
- enable_gzip_compression: bool = False) -> None:
83
+
84
+ ERROR_MSG_DISABLE_SSL = (
85
+ 'The connection failed because the SSL certificate is not valid. To use a self-signed '
86
+ 'certificate, disable verification of the server\'s SSL certificate by invoking the '
87
+ 'set_disable_ssl_verification(True) on your service instance and/ or use the '
88
+ 'disable_ssl_verification option of the authenticator.'
89
+ )
90
+
91
+ def __init__(
92
+ self,
93
+ *,
94
+ service_url: str = None,
95
+ authenticator: Optional[Authenticator] = None,
96
+ disable_ssl_verification: bool = False,
97
+ enable_gzip_compression: bool = False,
98
+ ) -> None:
87
99
  self.set_service_url(service_url)
88
100
  self.http_client = requests.Session()
89
101
  self.http_config = {}
@@ -92,9 +104,9 @@ class BaseService:
92
104
  self.disable_ssl_verification = disable_ssl_verification
93
105
  self.default_headers = None
94
106
  self.enable_gzip_compression = enable_gzip_compression
95
- self._set_user_agent_header(self._build_user_agent())
107
+ self._set_user_agent_header(_build_user_agent())
96
108
  self.retry_config = None
97
- self.http_adapter = SSLHTTPAdapter()
109
+ self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification)
98
110
  if not self.authenticator:
99
111
  raise ValueError('authenticator must be provided')
100
112
  if not isinstance(self.authenticator, Authenticator):
@@ -102,50 +114,48 @@ class BaseService:
102
114
 
103
115
  self.http_client.mount('http://', self.http_adapter)
104
116
  self.http_client.mount('https://', self.http_adapter)
105
-
106
- def enable_retries(self, max_retries: int = 4, retry_interval: float = 1.0) -> None:
117
+ # If debug logging is requested, then trigger HTTP message logging as well.
118
+ if logger.isEnabledFor(logging.DEBUG):
119
+ client.HTTPConnection.debuglevel = 1
120
+ # Replace the `print` function in the HTTPClient module to
121
+ # use the debug logger instead of the bare Python print.
122
+ client.print = lambda *args: logger.debug(LoggingFilter.filter_message(" ".join(args)))
123
+
124
+ def enable_retries(self, max_retries: int = 4, retry_interval: float = 30.0) -> None:
107
125
  """Enable automatic retries on the underlying http client used by the BaseService instance.
108
126
 
109
127
  Args:
110
128
  max_retries: the maximum number of retries to attempt for a failed retryable request
111
- retry_interval: the default wait time (in seconds) to use for the first retry attempt.
129
+ retry_interval: the maximum wait time (in seconds) to use for retry attempts.
112
130
  In general, if a response includes the Retry-After header, that will be used for
113
131
  the wait time associated with the retry attempt. If the Retry-After header is not
114
- present, then the wait time is based on the retry_interval and retry attempt number:
115
- wait_time = retry_interval * (2 ^ (n-1)), where n is the retry attempt number
132
+ present, then the wait time is based on an exponential backoff policy with a maximum
133
+ backoff time of "retry_interval".
116
134
  """
117
135
  self.retry_config = Retry(
118
136
  total=max_retries,
119
- backoff_factor=retry_interval,
137
+ backoff_factor=1.0,
138
+ backoff_max=retry_interval,
120
139
  # List of HTTP status codes to retry on in addition to Timeout/Connection Errors
121
140
  status_forcelist=[429, 500, 502, 503, 504],
122
141
  # List of HTTP methods to retry on
123
142
  # Omitting this will default to all methods except POST
124
- allowed_methods=['HEAD', 'GET', 'PUT',
125
- 'DELETE', 'OPTIONS', 'TRACE', 'POST']
143
+ allowed_methods=['HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST'],
144
+ )
145
+ self.http_adapter = SSLHTTPAdapter(
146
+ max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification
126
147
  )
127
- self.http_adapter = SSLHTTPAdapter(max_retries=self.retry_config)
128
148
  self.http_client.mount('http://', self.http_adapter)
129
149
  self.http_client.mount('https://', self.http_adapter)
150
+ logger.debug('Enabled retries; max_retries=%d, max_retry_interval=%f', max_retries, retry_interval)
130
151
 
131
152
  def disable_retries(self):
132
153
  """Remove retry config from http_adapter"""
133
154
  self.retry_config = None
134
- self.http_adapter = SSLHTTPAdapter()
155
+ self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification)
135
156
  self.http_client.mount('http://', self.http_adapter)
136
157
  self.http_client.mount('https://', self.http_adapter)
137
-
138
- @staticmethod
139
- def _get_system_info() -> str:
140
- return '{0} {1} {2}'.format(
141
- platform.system(), # OS
142
- platform.release(), # OS version
143
- platform.python_version() # Python version
144
- )
145
-
146
- def _build_user_agent(self) -> str:
147
- return '{0}-{1} {2}'.format(self.SDK_NAME, __version__,
148
- self._get_system_info())
158
+ logger.debug('Disabled retries')
149
159
 
150
160
  def configure_service(self, service_name: str) -> None:
151
161
  """Look for external configuration of a service. Set service properties.
@@ -164,27 +174,27 @@ class BaseService:
164
174
  if not isinstance(service_name, str):
165
175
  raise ValueError('Service_name must be of type string.')
166
176
 
177
+ logger.debug('Configuring BaseService instance with service name: %s', service_name)
178
+
167
179
  config = read_external_sources(service_name)
168
180
  if config.get('URL'):
169
181
  self.set_service_url(config.get('URL'))
170
182
  if config.get('DISABLE_SSL'):
171
- self.set_disable_ssl_verification(
172
- config.get('DISABLE_SSL').lower() == 'true')
183
+ self.set_disable_ssl_verification(config.get('DISABLE_SSL').lower() == 'true')
173
184
  if config.get('ENABLE_GZIP'):
174
- self.set_enable_gzip_compression(
175
- config.get('ENABLE_GZIP').lower() == 'true')
185
+ self.set_enable_gzip_compression(config.get('ENABLE_GZIP').lower() == 'true')
176
186
  if config.get('ENABLE_RETRIES'):
177
187
  if config.get('ENABLE_RETRIES').lower() == 'true':
178
188
  kwargs = {}
179
189
  if config.get('MAX_RETRIES'):
180
190
  kwargs["max_retries"] = int(config.get('MAX_RETRIES'))
181
191
  if config.get('RETRY_INTERVAL'):
182
- kwargs["retry_interval"] = float(
183
- config.get('RETRY_INTERVAL'))
192
+ kwargs["retry_interval"] = float(config.get('RETRY_INTERVAL'))
184
193
  self.enable_retries(**kwargs)
185
194
 
186
195
  def _set_user_agent_header(self, user_agent_string: str) -> None:
187
196
  self.user_agent_header = {'User-Agent': user_agent_string}
197
+ logger.debug('Set User-Agent: %s', user_agent_string)
188
198
 
189
199
  def set_http_config(self, http_config: dict) -> None:
190
200
  """Sets the http config dictionary.
@@ -199,10 +209,11 @@ class BaseService:
199
209
  """
200
210
  if isinstance(http_config, dict):
201
211
  self.http_config = http_config
202
- if (self.authenticator
203
- and hasattr(self.authenticator, 'token_manager')
204
- and isinstance(self.authenticator.token_manager,
205
- TokenManager)):
212
+ if (
213
+ self.authenticator
214
+ and hasattr(self.authenticator, 'token_manager')
215
+ and isinstance(self.authenticator.token_manager, TokenManager)
216
+ ):
206
217
  self.authenticator.token_manager.http_config = http_config
207
218
  else:
208
219
  raise TypeError("http_config parameter must be a dictionary")
@@ -214,8 +225,19 @@ class BaseService:
214
225
  Keyword Arguments:
215
226
  status: set to true to disable ssl verification (default: {False})
216
227
  """
228
+ if self.disable_ssl_verification == status:
229
+ # Do nothing if the state doesn't change.
230
+ return
231
+
217
232
  self.disable_ssl_verification = status
218
233
 
234
+ self.http_adapter = SSLHTTPAdapter(
235
+ max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification
236
+ )
237
+ self.http_client.mount('http://', self.http_adapter)
238
+ self.http_client.mount('https://', self.http_adapter)
239
+ logger.debug('Disabled SSL verification in HTTP client')
240
+
219
241
  def set_service_url(self, service_url: str) -> None:
220
242
  """Set the url the service will make HTTP requests too.
221
243
 
@@ -233,6 +255,7 @@ class BaseService:
233
255
  if service_url is not None:
234
256
  service_url = service_url.rstrip('/')
235
257
  self.service_url = service_url
258
+ logger.debug('Set service URL: %s', service_url)
236
259
 
237
260
  def get_http_client(self) -> requests.sessions.Session:
238
261
  """Get the http client session currently used by the service.
@@ -251,8 +274,7 @@ class BaseService:
251
274
  if isinstance(http_client, requests.sessions.Session):
252
275
  self.http_client = http_client
253
276
  else:
254
- raise TypeError(
255
- "http_client parameter must be a requests.sessions.Session")
277
+ raise TypeError("http_client parameter must be a requests.sessions.Session")
256
278
 
257
279
  def get_authenticator(self) -> Authenticator:
258
280
  """Get the authenticator currently used by the service.
@@ -296,7 +318,7 @@ class BaseService:
296
318
  # Check to see if the caller specified the 'stream' argument.
297
319
  stream_response = kwargs.get('stream') or False
298
320
 
299
- # Remove the keys we set manually, don't let the user to overwrite these.
321
+ # Remove the keys we set manually, don't let the user overwrite these.
300
322
  reserved_keys = ['method', 'url', 'headers', 'params', 'cookies']
301
323
  silent_keys = ['headers']
302
324
  for key in reserved_keys:
@@ -305,35 +327,44 @@ class BaseService:
305
327
  if key not in silent_keys:
306
328
  logger.warning('"%s" has been removed from the request', key)
307
329
  try:
308
- response = self.http_client.request(**request,
309
- cookies=self.jar,
310
- **kwargs)
330
+ logger.debug('Sending HTTP request message')
311
331
 
332
+ response = self.http_client.request(**request, cookies=self.jar, **kwargs)
333
+
334
+ logger.debug('Received HTTP response message, status code %d', response.status_code)
335
+
336
+ # Process a "success" response.
312
337
  if 200 <= response.status_code <= 299:
313
338
  if response.status_code == 204 or request['method'] == 'HEAD':
314
- # There is no body content for a HEAD request or a 204 response
339
+ # There is no body content for a HEAD response or a 204 response.
315
340
  result = None
316
341
  elif stream_response:
317
342
  result = response
318
343
  elif not response.text:
319
344
  result = None
320
- else:
345
+ elif is_json_mimetype(response.headers.get('Content-Type')):
346
+ # If this is a JSON response, then try to unmarshal it.
321
347
  try:
322
- result = response.json()
323
- except:
324
- result = response
325
- return DetailedResponse(response=result,
326
- headers=response.headers,
327
- status_code=response.status_code)
348
+ result = response.json(strict=False)
349
+ except JSONDecodeError as err:
350
+ raise ApiException(
351
+ code=response.status_code,
352
+ http_response=response,
353
+ message='Error processing the HTTP response',
354
+ ) from err
355
+ else:
356
+ # Non-JSON response, just use response body as-is.
357
+ result = response
358
+
359
+ return DetailedResponse(response=result, headers=response.headers, status_code=response.status_code)
328
360
 
361
+ # Received error status code from server, raise an APIException.
329
362
  raise ApiException(response.status_code, http_response=response)
330
363
  except requests.exceptions.SSLError:
331
364
  logger.exception(self.ERROR_MSG_DISABLE_SSL)
332
365
  raise
333
366
 
334
- def set_enable_gzip_compression(self,
335
- should_enable_compression: bool = False
336
- ) -> None:
367
+ def set_enable_gzip_compression(self, should_enable_compression: bool = False) -> None:
337
368
  """Set value to enable gzip compression on request bodies"""
338
369
  self.enable_gzip_compression = should_enable_compression
339
370
 
@@ -341,18 +372,17 @@ class BaseService:
341
372
  """Get value for enabling gzip compression on request bodies"""
342
373
  return self.enable_gzip_compression
343
374
 
344
- def prepare_request(self,
345
- method: str,
346
- url: str,
347
- *,
348
- headers: Optional[dict] = None,
349
- params: Optional[dict] = None,
350
- data: Optional[Union[str, dict]] = None,
351
- files: Optional[Union[Dict[str, Tuple[str]],
352
- List[Tuple[str,
353
- Tuple[str,
354
- ...]]]]] = None,
355
- **kwargs) -> dict:
375
+ def prepare_request(
376
+ self,
377
+ method: str,
378
+ url: str,
379
+ *,
380
+ headers: Optional[dict] = None,
381
+ params: Optional[dict] = None,
382
+ data: Optional[Union[str, dict]] = None,
383
+ files: Optional[Union[Dict[str, Tuple[str]], List[Tuple[str, Tuple[str, ...]]]]] = None,
384
+ **kwargs,
385
+ ) -> dict:
356
386
  """Build a dict that represents an HTTP service request.
357
387
 
358
388
  Clean up headers, add default http configuration, convert data
@@ -363,8 +393,10 @@ class BaseService:
363
393
  url: The origin + pathname according to WHATWG spec.
364
394
 
365
395
  Keyword Arguments:
366
- headers: Headers of the request.
367
- params: Querystring data to be appended to the url.
396
+ headers: A dictionary containing the headers to be included in the request.
397
+ Entries with a value of None will be ignored (excluded).
398
+ params: A dictionary containing the query parameters to be included in the request.
399
+ Entries with a value of None will be ignored (excluded).
368
400
  data: The request body. Converted to json if a dict.
369
401
  files: 'files' can be a dictionary (i.e { '<part-name>': (<tuple>)}),
370
402
  or a list of tuples [ (<part-name>, (<tuple>))... ]
@@ -409,14 +441,16 @@ class BaseService:
409
441
  self.authenticator.authenticate(request)
410
442
 
411
443
  # Compress the request body if applicable
412
- if (self.get_enable_gzip_compression()
413
- and 'content-encoding' not in headers
414
- and request['data'] is not None):
444
+ if self.get_enable_gzip_compression() and 'content-encoding' not in headers and request['data'] is not None:
415
445
  headers['content-encoding'] = 'gzip'
416
- uncompressed_data = request['data']
417
- request_body = gzip.compress(uncompressed_data)
418
- request['data'] = request_body
419
446
  request['headers'] = headers
447
+ # If the provided data is a file-like object, we create `GzipStream` which will handle
448
+ # the compression on-the-fly when the requests package starts reading its content.
449
+ # This helps avoid OOM errors when the opened file is too big.
450
+ # In any other cases, we use the in memory compression directly from
451
+ # the `gzip` package for backward compatibility.
452
+ raw_data = request['data']
453
+ request['data'] = GzipStream(raw_data) if isinstance(raw_data, io.IOBase) else gzip.compress(raw_data)
420
454
 
421
455
  # Next, we need to process the 'files' argument to try to fill in
422
456
  # any missing filenames where possible.
@@ -431,14 +465,14 @@ class BaseService:
431
465
  files = files.items()
432
466
  # Next, fill in any missing filenames from file tuples.
433
467
  for part_name, file_tuple in files:
434
- if file_tuple and len(
435
- file_tuple) == 3 and file_tuple[0] is None:
468
+ if file_tuple and len(file_tuple) == 3 and file_tuple[0] is None:
436
469
  file = file_tuple[1]
437
470
  if file and hasattr(file, 'name'):
438
471
  filename = basename(file.name)
439
472
  file_tuple = (filename, file_tuple[1], file_tuple[2])
440
473
  new_files.append((part_name, file_tuple))
441
474
  request['files'] = new_files
475
+ logger.debug('Prepared request [%s %s]', request['method'], request['url'])
442
476
  return request
443
477
 
444
478
  @staticmethod
@@ -15,7 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  import json
18
- from typing import Dict, Optional
18
+ from typing import Dict, Optional, Union
19
19
 
20
20
  import requests
21
21
 
@@ -26,44 +26,50 @@ class DetailedResponse:
26
26
  Keyword Args:
27
27
  response: The response to the service request, defaults to None.
28
28
  headers: The headers of the response, defaults to None.
29
- status_code: The status code of there response, defaults to None.
29
+ status_code: The status code of the response, defaults to None.
30
30
 
31
31
  Attributes:
32
- response (requests.Response): The response to the service request.
32
+ result (dict, requests.Response, None): The response to the service request.
33
33
  headers (dict): The headers of the response.
34
34
  status_code (int): The status code of the response.
35
35
 
36
36
  """
37
- def __init__(self,
38
- *,
39
- response: Optional[requests.Response] = None,
40
- headers: Optional[Dict[str, str]] = None,
41
- status_code: Optional[int] = None) -> None:
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ response: Optional[Union[dict, requests.Response]] = None,
42
+ headers: Optional[Dict[str, str]] = None,
43
+ status_code: Optional[int] = None,
44
+ ) -> None:
42
45
  self.result = response
43
46
  self.headers = headers
44
47
  self.status_code = status_code
45
48
 
46
- def get_result(self) -> requests.Response:
47
- """Get the HTTP response of the service request.
49
+ def get_result(self) -> Optional[Union[dict, requests.Response]]:
50
+ """Get the response returned by the service request.
48
51
 
49
52
  Returns:
50
- The response to the service request
53
+ The response to the service request. This could be one of the following:
54
+ 1. a dict that represents an instance of a response model
55
+ 2. a requests.Response instance if the operation returns a streamed response
56
+ 3. None if the server returned no response body
51
57
  """
52
58
  return self.result
53
59
 
54
- def get_headers(self) -> dict:
60
+ def get_headers(self) -> Optional[dict]:
55
61
  """The HTTP response headers of the service request.
56
62
 
57
63
  Returns:
58
- A dictionary of response headers
64
+ A dictionary of response headers or None if no headers are present.
59
65
  """
60
66
  return self.headers
61
67
 
62
- def get_status_code(self) -> int:
68
+ def get_status_code(self) -> Union[int, None]:
63
69
  """The HTTP status code of the service request.
64
70
 
65
71
  Returns:
66
- The status code of the service request.
72
+ The status code associated with the service request.
67
73
  """
68
74
  return self.status_code
69
75