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.
- ibm_cloud_sdk_core/__init__.py +1 -0
- ibm_cloud_sdk_core/api_exception.py +18 -4
- ibm_cloud_sdk_core/authenticators/__init__.py +1 -0
- ibm_cloud_sdk_core/authenticators/authenticator.py +2 -1
- ibm_cloud_sdk_core/authenticators/basic_authenticator.py +12 -7
- ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py +7 -2
- ibm_cloud_sdk_core/authenticators/container_authenticator.py +25 -16
- ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py +38 -23
- ibm_cloud_sdk_core/authenticators/iam_authenticator.py +22 -13
- ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py +10 -8
- ibm_cloud_sdk_core/authenticators/mcsp_authenticator.py +134 -0
- ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py +12 -11
- ibm_cloud_sdk_core/base_service.py +137 -103
- ibm_cloud_sdk_core/detailed_response.py +21 -15
- ibm_cloud_sdk_core/get_authenticator.py +35 -17
- ibm_cloud_sdk_core/http_adapter.py +28 -0
- ibm_cloud_sdk_core/logger.py +85 -0
- ibm_cloud_sdk_core/private_helpers.py +34 -0
- ibm_cloud_sdk_core/token_managers/container_token_manager.py +63 -33
- ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py +35 -22
- ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py +50 -21
- ibm_cloud_sdk_core/token_managers/iam_token_manager.py +24 -13
- ibm_cloud_sdk_core/token_managers/jwt_token_manager.py +3 -16
- ibm_cloud_sdk_core/token_managers/mcsp_token_manager.py +108 -0
- ibm_cloud_sdk_core/token_managers/token_manager.py +20 -23
- ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py +37 -18
- ibm_cloud_sdk_core/utils.py +127 -48
- ibm_cloud_sdk_core/version.py +1 -1
- ibm_cloud_sdk_core-3.21.0.dist-info/METADATA +159 -0
- ibm_cloud_sdk_core-3.21.0.dist-info/RECORD +35 -0
- {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.21.0.dist-info}/WHEEL +1 -1
- ibm_cloud_sdk_core-3.21.0.dist-info/top_level.txt +1 -0
- ibm_cloud_sdk_core-3.16.0.dist-info/METADATA +0 -112
- ibm_cloud_sdk_core-3.16.0.dist-info/RECORD +0 -52
- ibm_cloud_sdk_core-3.16.0.dist-info/top_level.txt +0 -3
- ibm_cloud_sdk_core-3.16.0.dist-info/zip-safe +0 -1
- test/__init__.py +0 -0
- test/test_api_exception.py +0 -73
- test/test_authenticator.py +0 -21
- test/test_base_service.py +0 -933
- test/test_basic_authenticator.py +0 -36
- test/test_bearer_authenticator.py +0 -28
- test/test_container_authenticator.py +0 -105
- test/test_container_token_manager.py +0 -283
- test/test_cp4d_authenticator.py +0 -171
- test/test_cp4d_token_manager.py +0 -56
- test/test_detailed_response.py +0 -57
- test/test_iam_authenticator.py +0 -157
- test/test_iam_token_manager.py +0 -362
- test/test_jwt_token_manager.py +0 -109
- test/test_no_auth_authenticator.py +0 -15
- test/test_token_manager.py +0 -84
- test/test_utils.py +0 -634
- test/test_vpc_instance_authenticator.py +0 -66
- test/test_vpc_instance_token_manager.py +0 -266
- test_integration/__init__.py +0 -0
- test_integration/test_cp4d_authenticator_integration.py +0 -45
- {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__(
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
18
|
+
import io
|
|
19
19
|
import logging
|
|
20
|
-
import
|
|
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 (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
logger
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
76
|
-
ERROR_MSG_DISABLE_SSL =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
115
|
-
|
|
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=
|
|
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
|
-
|
|
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 (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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:
|
|
367
|
-
|
|
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
|
|
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
|
|
29
|
+
status_code: The status code of the response, defaults to None.
|
|
30
30
|
|
|
31
31
|
Attributes:
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
72
|
+
The status code associated with the service request.
|
|
67
73
|
"""
|
|
68
74
|
return self.status_code
|
|
69
75
|
|