ibm-cloud-sdk-core 3.16.0__py3-none-any.whl → 3.20.6__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 +5 -6
- ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py +1 -1
- ibm_cloud_sdk_core/authenticators/container_authenticator.py +25 -16
- ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py +33 -21
- ibm_cloud_sdk_core/authenticators/iam_authenticator.py +22 -13
- ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py +5 -7
- ibm_cloud_sdk_core/authenticators/mcsp_authenticator.py +130 -0
- ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py +7 -10
- ibm_cloud_sdk_core/base_service.py +107 -91
- ibm_cloud_sdk_core/detailed_response.py +21 -15
- ibm_cloud_sdk_core/get_authenticator.py +28 -16
- ibm_cloud_sdk_core/http_adapter.py +28 -0
- ibm_cloud_sdk_core/private_helpers.py +34 -0
- ibm_cloud_sdk_core/token_managers/container_token_manager.py +61 -30
- ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py +30 -22
- ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py +43 -20
- 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 +102 -0
- ibm_cloud_sdk_core/token_managers/token_manager.py +13 -23
- ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py +33 -13
- ibm_cloud_sdk_core/utils.py +121 -46
- ibm_cloud_sdk_core/version.py +1 -1
- {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.20.6.dist-info}/METADATA +40 -28
- ibm_cloud_sdk_core-3.20.6.dist-info/RECORD +34 -0
- {ibm_cloud_sdk_core-3.16.0.dist-info → ibm_cloud_sdk_core-3.20.6.dist-info}/WHEEL +1 -1
- ibm_cloud_sdk_core-3.20.6.dist-info/top_level.txt +1 -0
- 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.20.6.dist-info}/LICENSE +0 -0
|
@@ -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,9 +15,9 @@
|
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
17
|
import gzip
|
|
18
|
+
import io
|
|
18
19
|
import json as json_import
|
|
19
20
|
import logging
|
|
20
|
-
import platform
|
|
21
21
|
from http.cookiejar import CookieJar
|
|
22
22
|
from os.path import basename
|
|
23
23
|
from typing import Dict, List, Optional, Tuple, Union
|
|
@@ -25,15 +25,23 @@ from urllib3.util.retry import Retry
|
|
|
25
25
|
|
|
26
26
|
import requests
|
|
27
27
|
from requests.structures import CaseInsensitiveDict
|
|
28
|
+
from requests.exceptions import JSONDecodeError
|
|
28
29
|
|
|
29
30
|
from ibm_cloud_sdk_core.authenticators import Authenticator
|
|
30
31
|
from .api_exception import ApiException
|
|
31
32
|
from .detailed_response import DetailedResponse
|
|
33
|
+
from .http_adapter import SSLHTTPAdapter
|
|
32
34
|
from .token_managers.token_manager import TokenManager
|
|
33
|
-
from .utils import (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
from .utils import (
|
|
36
|
+
has_bad_first_or_last_char,
|
|
37
|
+
is_json_mimetype,
|
|
38
|
+
remove_null_values,
|
|
39
|
+
cleanup_values,
|
|
40
|
+
read_external_sources,
|
|
41
|
+
strip_extra_slashes,
|
|
42
|
+
GzipStream,
|
|
43
|
+
)
|
|
44
|
+
from .private_helpers import _build_user_agent
|
|
37
45
|
|
|
38
46
|
# Uncomment this to enable http debugging
|
|
39
47
|
# import http.client as http_client
|
|
@@ -43,8 +51,8 @@ from .version import __version__
|
|
|
43
51
|
logger = logging.getLogger(__name__)
|
|
44
52
|
|
|
45
53
|
|
|
46
|
-
#pylint: disable=too-many-instance-attributes
|
|
47
|
-
#pylint: disable=too-many-locals
|
|
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: 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):
|
|
@@ -103,50 +115,40 @@ class BaseService:
|
|
|
103
115
|
self.http_client.mount('http://', self.http_adapter)
|
|
104
116
|
self.http_client.mount('https://', self.http_adapter)
|
|
105
117
|
|
|
106
|
-
def enable_retries(self, max_retries: int = 4, retry_interval: float =
|
|
118
|
+
def enable_retries(self, max_retries: int = 4, retry_interval: float = 30.0) -> None:
|
|
107
119
|
"""Enable automatic retries on the underlying http client used by the BaseService instance.
|
|
108
120
|
|
|
109
121
|
Args:
|
|
110
122
|
max_retries: the maximum number of retries to attempt for a failed retryable request
|
|
111
|
-
retry_interval: the
|
|
123
|
+
retry_interval: the maximum wait time (in seconds) to use for retry attempts.
|
|
112
124
|
In general, if a response includes the Retry-After header, that will be used for
|
|
113
125
|
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
|
-
|
|
126
|
+
present, then the wait time is based on an exponential backoff policy with a maximum
|
|
127
|
+
backoff time of "retry_interval".
|
|
116
128
|
"""
|
|
117
129
|
self.retry_config = Retry(
|
|
118
130
|
total=max_retries,
|
|
119
|
-
backoff_factor=
|
|
131
|
+
backoff_factor=1.0,
|
|
132
|
+
backoff_max=retry_interval,
|
|
120
133
|
# List of HTTP status codes to retry on in addition to Timeout/Connection Errors
|
|
121
134
|
status_forcelist=[429, 500, 502, 503, 504],
|
|
122
135
|
# List of HTTP methods to retry on
|
|
123
136
|
# Omitting this will default to all methods except POST
|
|
124
|
-
allowed_methods=['HEAD', 'GET', 'PUT',
|
|
125
|
-
|
|
137
|
+
allowed_methods=['HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST'],
|
|
138
|
+
)
|
|
139
|
+
self.http_adapter = SSLHTTPAdapter(
|
|
140
|
+
max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification
|
|
126
141
|
)
|
|
127
|
-
self.http_adapter = SSLHTTPAdapter(max_retries=self.retry_config)
|
|
128
142
|
self.http_client.mount('http://', self.http_adapter)
|
|
129
143
|
self.http_client.mount('https://', self.http_adapter)
|
|
130
144
|
|
|
131
145
|
def disable_retries(self):
|
|
132
146
|
"""Remove retry config from http_adapter"""
|
|
133
147
|
self.retry_config = None
|
|
134
|
-
self.http_adapter = SSLHTTPAdapter()
|
|
148
|
+
self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification)
|
|
135
149
|
self.http_client.mount('http://', self.http_adapter)
|
|
136
150
|
self.http_client.mount('https://', self.http_adapter)
|
|
137
151
|
|
|
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())
|
|
149
|
-
|
|
150
152
|
def configure_service(self, service_name: str) -> None:
|
|
151
153
|
"""Look for external configuration of a service. Set service properties.
|
|
152
154
|
|
|
@@ -168,19 +170,16 @@ class BaseService:
|
|
|
168
170
|
if config.get('URL'):
|
|
169
171
|
self.set_service_url(config.get('URL'))
|
|
170
172
|
if config.get('DISABLE_SSL'):
|
|
171
|
-
self.set_disable_ssl_verification(
|
|
172
|
-
config.get('DISABLE_SSL').lower() == 'true')
|
|
173
|
+
self.set_disable_ssl_verification(config.get('DISABLE_SSL').lower() == 'true')
|
|
173
174
|
if config.get('ENABLE_GZIP'):
|
|
174
|
-
self.set_enable_gzip_compression(
|
|
175
|
-
config.get('ENABLE_GZIP').lower() == 'true')
|
|
175
|
+
self.set_enable_gzip_compression(config.get('ENABLE_GZIP').lower() == 'true')
|
|
176
176
|
if config.get('ENABLE_RETRIES'):
|
|
177
177
|
if config.get('ENABLE_RETRIES').lower() == 'true':
|
|
178
178
|
kwargs = {}
|
|
179
179
|
if config.get('MAX_RETRIES'):
|
|
180
180
|
kwargs["max_retries"] = int(config.get('MAX_RETRIES'))
|
|
181
181
|
if config.get('RETRY_INTERVAL'):
|
|
182
|
-
kwargs["retry_interval"] = float(
|
|
183
|
-
config.get('RETRY_INTERVAL'))
|
|
182
|
+
kwargs["retry_interval"] = float(config.get('RETRY_INTERVAL'))
|
|
184
183
|
self.enable_retries(**kwargs)
|
|
185
184
|
|
|
186
185
|
def _set_user_agent_header(self, user_agent_string: str) -> None:
|
|
@@ -199,10 +198,11 @@ class BaseService:
|
|
|
199
198
|
"""
|
|
200
199
|
if isinstance(http_config, dict):
|
|
201
200
|
self.http_config = http_config
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
if (
|
|
202
|
+
self.authenticator
|
|
203
|
+
and hasattr(self.authenticator, 'token_manager')
|
|
204
|
+
and isinstance(self.authenticator.token_manager, TokenManager)
|
|
205
|
+
):
|
|
206
206
|
self.authenticator.token_manager.http_config = http_config
|
|
207
207
|
else:
|
|
208
208
|
raise TypeError("http_config parameter must be a dictionary")
|
|
@@ -214,8 +214,18 @@ class BaseService:
|
|
|
214
214
|
Keyword Arguments:
|
|
215
215
|
status: set to true to disable ssl verification (default: {False})
|
|
216
216
|
"""
|
|
217
|
+
if self.disable_ssl_verification == status:
|
|
218
|
+
# Do nothing if the state doesn't change.
|
|
219
|
+
return
|
|
220
|
+
|
|
217
221
|
self.disable_ssl_verification = status
|
|
218
222
|
|
|
223
|
+
self.http_adapter = SSLHTTPAdapter(
|
|
224
|
+
max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification
|
|
225
|
+
)
|
|
226
|
+
self.http_client.mount('http://', self.http_adapter)
|
|
227
|
+
self.http_client.mount('https://', self.http_adapter)
|
|
228
|
+
|
|
219
229
|
def set_service_url(self, service_url: str) -> None:
|
|
220
230
|
"""Set the url the service will make HTTP requests too.
|
|
221
231
|
|
|
@@ -251,8 +261,7 @@ class BaseService:
|
|
|
251
261
|
if isinstance(http_client, requests.sessions.Session):
|
|
252
262
|
self.http_client = http_client
|
|
253
263
|
else:
|
|
254
|
-
raise TypeError(
|
|
255
|
-
"http_client parameter must be a requests.sessions.Session")
|
|
264
|
+
raise TypeError("http_client parameter must be a requests.sessions.Session")
|
|
256
265
|
|
|
257
266
|
def get_authenticator(self) -> Authenticator:
|
|
258
267
|
"""Get the authenticator currently used by the service.
|
|
@@ -305,35 +314,40 @@ class BaseService:
|
|
|
305
314
|
if key not in silent_keys:
|
|
306
315
|
logger.warning('"%s" has been removed from the request', key)
|
|
307
316
|
try:
|
|
308
|
-
response = self.http_client.request(**request,
|
|
309
|
-
cookies=self.jar,
|
|
310
|
-
**kwargs)
|
|
317
|
+
response = self.http_client.request(**request, cookies=self.jar, **kwargs)
|
|
311
318
|
|
|
319
|
+
# Process a "success" response.
|
|
312
320
|
if 200 <= response.status_code <= 299:
|
|
313
321
|
if response.status_code == 204 or request['method'] == 'HEAD':
|
|
314
|
-
# There is no body content for a HEAD
|
|
322
|
+
# There is no body content for a HEAD response or a 204 response.
|
|
315
323
|
result = None
|
|
316
324
|
elif stream_response:
|
|
317
325
|
result = response
|
|
318
326
|
elif not response.text:
|
|
319
327
|
result = None
|
|
320
|
-
|
|
328
|
+
elif is_json_mimetype(response.headers.get('Content-Type')):
|
|
329
|
+
# If this is a JSON response, then try to unmarshal it.
|
|
321
330
|
try:
|
|
322
|
-
result = response.json()
|
|
323
|
-
except:
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
331
|
+
result = response.json(strict=False)
|
|
332
|
+
except JSONDecodeError as err:
|
|
333
|
+
raise ApiException(
|
|
334
|
+
code=response.status_code,
|
|
335
|
+
http_response=response,
|
|
336
|
+
message='Error processing the HTTP response',
|
|
337
|
+
) from err
|
|
338
|
+
else:
|
|
339
|
+
# Non-JSON response, just use response body as-is.
|
|
340
|
+
result = response
|
|
341
|
+
|
|
342
|
+
return DetailedResponse(response=result, headers=response.headers, status_code=response.status_code)
|
|
328
343
|
|
|
344
|
+
# Received error status code from server, raise an APIException.
|
|
329
345
|
raise ApiException(response.status_code, http_response=response)
|
|
330
346
|
except requests.exceptions.SSLError:
|
|
331
347
|
logger.exception(self.ERROR_MSG_DISABLE_SSL)
|
|
332
348
|
raise
|
|
333
349
|
|
|
334
|
-
def set_enable_gzip_compression(self,
|
|
335
|
-
should_enable_compression: bool = False
|
|
336
|
-
) -> None:
|
|
350
|
+
def set_enable_gzip_compression(self, should_enable_compression: bool = False) -> None:
|
|
337
351
|
"""Set value to enable gzip compression on request bodies"""
|
|
338
352
|
self.enable_gzip_compression = should_enable_compression
|
|
339
353
|
|
|
@@ -341,18 +355,17 @@ class BaseService:
|
|
|
341
355
|
"""Get value for enabling gzip compression on request bodies"""
|
|
342
356
|
return self.enable_gzip_compression
|
|
343
357
|
|
|
344
|
-
def prepare_request(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
**kwargs) -> dict:
|
|
358
|
+
def prepare_request(
|
|
359
|
+
self,
|
|
360
|
+
method: str,
|
|
361
|
+
url: str,
|
|
362
|
+
*,
|
|
363
|
+
headers: Optional[dict] = None,
|
|
364
|
+
params: Optional[dict] = None,
|
|
365
|
+
data: Optional[Union[str, dict]] = None,
|
|
366
|
+
files: Optional[Union[Dict[str, Tuple[str]], List[Tuple[str, Tuple[str, ...]]]]] = None,
|
|
367
|
+
**kwargs,
|
|
368
|
+
) -> dict:
|
|
356
369
|
"""Build a dict that represents an HTTP service request.
|
|
357
370
|
|
|
358
371
|
Clean up headers, add default http configuration, convert data
|
|
@@ -363,8 +376,10 @@ class BaseService:
|
|
|
363
376
|
url: The origin + pathname according to WHATWG spec.
|
|
364
377
|
|
|
365
378
|
Keyword Arguments:
|
|
366
|
-
headers:
|
|
367
|
-
|
|
379
|
+
headers: A dictionary containing the headers to be included in the request.
|
|
380
|
+
Entries with a value of None will be ignored (excluded).
|
|
381
|
+
params: A dictionary containing the query parameters to be included in the request.
|
|
382
|
+
Entries with a value of None will be ignored (excluded).
|
|
368
383
|
data: The request body. Converted to json if a dict.
|
|
369
384
|
files: 'files' can be a dictionary (i.e { '<part-name>': (<tuple>)}),
|
|
370
385
|
or a list of tuples [ (<part-name>, (<tuple>))... ]
|
|
@@ -409,14 +424,16 @@ class BaseService:
|
|
|
409
424
|
self.authenticator.authenticate(request)
|
|
410
425
|
|
|
411
426
|
# Compress the request body if applicable
|
|
412
|
-
if
|
|
413
|
-
and 'content-encoding' not in headers
|
|
414
|
-
and request['data'] is not None):
|
|
427
|
+
if self.get_enable_gzip_compression() and 'content-encoding' not in headers and request['data'] is not None:
|
|
415
428
|
headers['content-encoding'] = 'gzip'
|
|
416
|
-
uncompressed_data = request['data']
|
|
417
|
-
request_body = gzip.compress(uncompressed_data)
|
|
418
|
-
request['data'] = request_body
|
|
419
429
|
request['headers'] = headers
|
|
430
|
+
# If the provided data is a file-like object, we create `GzipStream` which will handle
|
|
431
|
+
# the compression on-the-fly when the requests package starts reading its content.
|
|
432
|
+
# This helps avoid OOM errors when the opened file is too big.
|
|
433
|
+
# In any other cases, we use the in memory compression directly from
|
|
434
|
+
# the `gzip` package for backward compatibility.
|
|
435
|
+
raw_data = request['data']
|
|
436
|
+
request['data'] = GzipStream(raw_data) if isinstance(raw_data, io.IOBase) else gzip.compress(raw_data)
|
|
420
437
|
|
|
421
438
|
# Next, we need to process the 'files' argument to try to fill in
|
|
422
439
|
# any missing filenames where possible.
|
|
@@ -431,8 +448,7 @@ class BaseService:
|
|
|
431
448
|
files = files.items()
|
|
432
449
|
# Next, fill in any missing filenames from file tuples.
|
|
433
450
|
for part_name, file_tuple in files:
|
|
434
|
-
if file_tuple and len(
|
|
435
|
-
file_tuple) == 3 and file_tuple[0] is None:
|
|
451
|
+
if file_tuple and len(file_tuple) == 3 and file_tuple[0] is None:
|
|
436
452
|
file = file_tuple[1]
|
|
437
453
|
if file and hasattr(file, 'name'):
|
|
438
454
|
filename = basename(file.name)
|
|
@@ -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
|
|
|
@@ -14,9 +14,17 @@
|
|
|
14
14
|
# See the License for the specific language governing permissions and
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
|
-
from .authenticators import (
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
from .authenticators import (
|
|
18
|
+
Authenticator,
|
|
19
|
+
BasicAuthenticator,
|
|
20
|
+
BearerTokenAuthenticator,
|
|
21
|
+
ContainerAuthenticator,
|
|
22
|
+
CloudPakForDataAuthenticator,
|
|
23
|
+
IAMAuthenticator,
|
|
24
|
+
NoAuthAuthenticator,
|
|
25
|
+
VPCInstanceAuthenticator,
|
|
26
|
+
MCSPAuthenticator,
|
|
27
|
+
)
|
|
20
28
|
from .utils import read_external_sources
|
|
21
29
|
|
|
22
30
|
|
|
@@ -59,12 +67,9 @@ def __construct_authenticator(config: dict) -> Authenticator:
|
|
|
59
67
|
authenticator = None
|
|
60
68
|
|
|
61
69
|
if auth_type == Authenticator.AUTHTYPE_BASIC.lower():
|
|
62
|
-
authenticator = BasicAuthenticator(
|
|
63
|
-
username=config.get('USERNAME'),
|
|
64
|
-
password=config.get('PASSWORD'))
|
|
70
|
+
authenticator = BasicAuthenticator(username=config.get('USERNAME'), password=config.get('PASSWORD'))
|
|
65
71
|
elif auth_type == Authenticator.AUTHTYPE_BEARERTOKEN.lower():
|
|
66
|
-
authenticator = BearerTokenAuthenticator(
|
|
67
|
-
bearer_token=config.get('BEARER_TOKEN'))
|
|
72
|
+
authenticator = BearerTokenAuthenticator(bearer_token=config.get('BEARER_TOKEN'))
|
|
68
73
|
elif auth_type == Authenticator.AUTHTYPE_CONTAINER.lower():
|
|
69
74
|
authenticator = ContainerAuthenticator(
|
|
70
75
|
cr_token_filename=config.get('CR_TOKEN_FILENAME'),
|
|
@@ -73,30 +78,37 @@ def __construct_authenticator(config: dict) -> Authenticator:
|
|
|
73
78
|
url=config.get('AUTH_URL'),
|
|
74
79
|
client_id=config.get('CLIENT_ID'),
|
|
75
80
|
client_secret=config.get('CLIENT_SECRET'),
|
|
76
|
-
disable_ssl_verification=config.get(
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
|
|
82
|
+
scope=config.get('SCOPE'),
|
|
83
|
+
)
|
|
79
84
|
elif auth_type == Authenticator.AUTHTYPE_CP4D.lower():
|
|
80
85
|
authenticator = CloudPakForDataAuthenticator(
|
|
81
86
|
username=config.get('USERNAME'),
|
|
82
87
|
password=config.get('PASSWORD'),
|
|
83
88
|
url=config.get('AUTH_URL'),
|
|
84
89
|
apikey=config.get('APIKEY'),
|
|
85
|
-
disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true'
|
|
90
|
+
disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
|
|
91
|
+
)
|
|
86
92
|
elif auth_type == Authenticator.AUTHTYPE_IAM.lower() and config.get('APIKEY'):
|
|
87
93
|
authenticator = IAMAuthenticator(
|
|
88
94
|
apikey=config.get('APIKEY'),
|
|
89
95
|
url=config.get('AUTH_URL'),
|
|
90
96
|
client_id=config.get('CLIENT_ID'),
|
|
91
97
|
client_secret=config.get('CLIENT_SECRET'),
|
|
92
|
-
disable_ssl_verification=config.get(
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true',
|
|
99
|
+
scope=config.get('SCOPE'),
|
|
100
|
+
)
|
|
95
101
|
elif auth_type == Authenticator.AUTHTYPE_VPC.lower():
|
|
96
102
|
authenticator = VPCInstanceAuthenticator(
|
|
97
103
|
iam_profile_crn=config.get('IAM_PROFILE_CRN'),
|
|
98
104
|
iam_profile_id=config.get('IAM_PROFILE_ID'),
|
|
99
|
-
url=config.get('AUTH_URL')
|
|
105
|
+
url=config.get('AUTH_URL'),
|
|
106
|
+
)
|
|
107
|
+
elif auth_type == Authenticator.AUTHTYPE_MCSP.lower():
|
|
108
|
+
authenticator = MCSPAuthenticator(
|
|
109
|
+
apikey=config.get('APIKEY'),
|
|
110
|
+
url=config.get('AUTH_URL'),
|
|
111
|
+
)
|
|
100
112
|
elif auth_type == Authenticator.AUTHTYPE_NOAUTH.lower():
|
|
101
113
|
authenticator = NoAuthAuthenticator()
|
|
102
114
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
|
|
3
|
+
from requests import certs
|
|
4
|
+
from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK
|
|
5
|
+
from urllib3.util.ssl_ import create_urllib3_context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SSLHTTPAdapter(HTTPAdapter):
|
|
9
|
+
"""Wraps the original HTTP adapter and adds additional SSL context."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, *args, **kwargs):
|
|
12
|
+
self._disable_ssl_verification = kwargs.pop('_disable_ssl_verification', None)
|
|
13
|
+
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs):
|
|
17
|
+
"""Create and use custom SSL configuration."""
|
|
18
|
+
|
|
19
|
+
ssl_context = create_urllib3_context()
|
|
20
|
+
# NOTE: https://github.com/psf/requests/pull/6731/files#r1622893724
|
|
21
|
+
ssl_context.load_verify_locations(certs.where())
|
|
22
|
+
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
23
|
+
|
|
24
|
+
if self._disable_ssl_verification:
|
|
25
|
+
ssl_context.check_hostname = False
|
|
26
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
27
|
+
|
|
28
|
+
super().init_poolmanager(connections, maxsize, block, ssl_context=ssl_context, **pool_kwargs)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
# Copyright 2024 IBM All Rights Reserved.
|
|
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
|
+
# from ibm_cloud_sdk_core.authenticators import Authenticator
|
|
17
|
+
|
|
18
|
+
import platform
|
|
19
|
+
from .version import __version__
|
|
20
|
+
|
|
21
|
+
SDK_NAME = 'ibm-python-sdk-core'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_system_info() -> str:
|
|
25
|
+
return 'os.name={0} os.version={1} python.version={2}'.format(
|
|
26
|
+
platform.system(), platform.release(), platform.python_version()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_user_agent(component: str = None) -> str:
|
|
31
|
+
sub_component = ""
|
|
32
|
+
if component is not None:
|
|
33
|
+
sub_component = '/{0}'.format(component)
|
|
34
|
+
return '{0}{1}-{2} {3}'.format(SDK_NAME, sub_component, __version__, _get_system_info())
|