diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/changelog python-ibm-cloud-sdk-core-3.12.0/debian/changelog --- python-ibm-cloud-sdk-core-1.6.2/debian/changelog 2020-02-24 20:39:02.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/changelog 2021-11-06 23:14:19.000000000 +0000 @@ -1,3 +1,37 @@ +python-ibm-cloud-sdk-core (3.12.0-2) unstable; urgency=low + + * Prevent installation of pytest cache artifacts. + + -- Michael Fladischer Sat, 06 Nov 2021 23:14:19 +0000 + +python-ibm-cloud-sdk-core (3.12.0-1) unstable; urgency=low + + [ Debian Janitor ] + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Ondřej Nový ] + * d/control: Update Maintainer field with new Debian Python Team + contact address. + * d/control: Update Vcs-* fields with new Debian Python Team Salsa + layout. + + [ Michael Fladischer ] + * New upstream release. + * Bump debhelper version to 13. + * Bump Standards-Version to 4.6.0.1. + * Use uscan version 4. + * Add patch to not install test packages. + * Clean up ibm_cloud_sdk_core.egg-info/top_level.txt to allow two + builds in a row. + * Add python3-pytest and python3-responses to Build-Depends. + * Run upstream tests during build (and skip those that require + credentials). + * Install testfiles using d/pybuild.testfiles. + * Enable upstream testsuite for autopkgtests. + + -- Michael Fladischer Sat, 06 Nov 2021 23:01:50 +0000 + python-ibm-cloud-sdk-core (1.6.2-1) unstable; urgency=low * New upstream release. diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/clean python-ibm-cloud-sdk-core-3.12.0/debian/clean --- python-ibm-cloud-sdk-core-1.6.2/debian/clean 2020-02-24 20:39:02.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/clean 2021-11-06 23:14:19.000000000 +0000 @@ -1,3 +1,4 @@ ibm_cloud_sdk_core.egg-info/PKG-INFO ibm_cloud_sdk_core.egg-info/SOURCES.txt ibm_cloud_sdk_core.egg-info/requires.txt +ibm_cloud_sdk_core.egg-info/top_level.txt diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/control python-ibm-cloud-sdk-core-3.12.0/debian/control --- python-ibm-cloud-sdk-core-1.6.2/debian/control 2020-02-24 20:39:02.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/control 2021-11-06 23:14:19.000000000 +0000 @@ -1,21 +1,23 @@ Source: python-ibm-cloud-sdk-core Section: python Priority: optional -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Team Uploaders: Michael Fladischer , Build-Depends: - debhelper-compat (= 12), + debhelper-compat (= 13), dh-python, python3-all, python3-dateutil, python3-jwt, + python3-pytest, + python3-responses, python3-requests, python3-setuptools, -Standards-Version: 4.5.0 +Standards-Version: 4.6.0.1 Homepage: https://github.com/IBM/python-sdk-core/ -Vcs-Browser: https://salsa.debian.org/python-team/modules/python-ibm-cloud-sdk-core -Vcs-Git: https://salsa.debian.org/python-team/modules/python-ibm-cloud-sdk-core.git +Vcs-Browser: https://salsa.debian.org/python-team/packages/python-ibm-cloud-sdk-core +Vcs-Git: https://salsa.debian.org/python-team/packages/python-ibm-cloud-sdk-core.git Testsuite: autopkgtest-pkg-python Rules-Requires-Root: no diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/patches/0001-Do-not-install-test-packages.patch python-ibm-cloud-sdk-core-3.12.0/debian/patches/0001-Do-not-install-test-packages.patch --- python-ibm-cloud-sdk-core-1.6.2/debian/patches/0001-Do-not-install-test-packages.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/patches/0001-Do-not-install-test-packages.patch 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1,28 @@ +From: Michael Fladischer +Date: Sat, 6 Nov 2021 21:12:02 +0000 +Subject: Do not install test packages. + +--- + setup.py | 9 ++++++++- + 1 file changed, 8 insertions(+), 1 deletion(-) + +diff --git a/setup.py b/setup.py +index 3983b6d..300f198 100644 +--- a/setup.py ++++ b/setup.py +@@ -62,7 +62,14 @@ setup(name='ibm-cloud-sdk-core', + long_description=readme, + long_description_content_type='text/markdown', + url='https://github.com/IBM/python-sdk-core', +- packages=find_packages(), ++ packages=find_packages( ++ exclude=( ++ 'tests', ++ 'tests.*', ++ 'test_integration', ++ 'test_integration.*', ++ ) ++ ), + include_package_data=True, + keywords='watson, ibm, cloud, ibm cloud services', + classifiers=[ diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/patches/series python-ibm-cloud-sdk-core-3.12.0/debian/patches/series --- python-ibm-cloud-sdk-core-1.6.2/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/patches/series 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1 @@ +0001-Do-not-install-test-packages.patch diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/pybuild.testfiles python-ibm-cloud-sdk-core-3.12.0/debian/pybuild.testfiles --- python-ibm-cloud-sdk-core-1.6.2/debian/pybuild.testfiles 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/pybuild.testfiles 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1,3 @@ +setup.cfg +setup.py +test diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/rules python-ibm-cloud-sdk-core-3.12.0/debian/rules --- python-ibm-cloud-sdk-core-1.6.2/debian/rules 2020-02-24 20:39:02.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/rules 2021-11-06 23:14:19.000000000 +0000 @@ -4,7 +4,31 @@ #export DH_VERBOSE=1 export PYBUILD_NAME=ibm-cloud-sdk-core -export PYBUILD_DISABLE=test +export PYBUILD_TEST_ARGS=test \ + -k "\ + not test_cwd and \ + not test_iam and \ + not test_gzip_compression_external and \ + not test_retry_config_external and \ + not test_files_dict and \ + not test_files_duplicate_parts and \ + not test_configure_service and \ + not test_request_token_auth_default and \ + not test_request_token_auth_in_ctor and \ + not test_request_token_auth_in_ctor_with_scope and \ + not test_get_token_success and \ + not test_request_token_success and \ + not test_authenticate_success and \ + not test_authenticate_fail_iam and \ + not test_client_id_and_secret and \ + not test_setter_methods and \ + not test_retrieve_cr_token_success and \ + not test_get_authenticator_from_credential_file and \ + not test_get_authenticator_from_credential_file_scope and \ + not test_read_external_sources_2 and \ + not test_files_list\ + " +export PYBUILD_AFTER_TEST=rm -rf {build_dir}/.pytest_cache %: dh $@ --with python3 --buildsystem=pybuild diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/tests/control python-ibm-cloud-sdk-core-3.12.0/debian/tests/control --- python-ibm-cloud-sdk-core-1.6.2/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/tests/control 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1,5 @@ +Tests: upstream +Depends: + @, + @builddeps@, +Restrictions: allow-stderr diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/tests/upstream python-ibm-cloud-sdk-core-3.12.0/debian/tests/upstream --- python-ibm-cloud-sdk-core-1.6.2/debian/tests/upstream 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/tests/upstream 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +cp -r test ${AUTOPKGTEST_TMP} +cd ${AUTOPKGTEST_TMP} +for p in $(py3versions -s); do + $p -m pytest test -k "\ + not test_cwd and \ + not test_iam and \ + not test_gzip_compression_external and \ + not test_retry_config_external and \ + not test_files_dict and \ + not test_files_duplicate_parts and \ + not test_configure_service and \ + not test_request_token_auth_default and \ + not test_request_token_auth_in_ctor and \ + not test_request_token_auth_in_ctor_with_scope and \ + not test_get_token_success and \ + not test_request_token_success and \ + not test_authenticate_success and \ + not test_authenticate_fail_iam and \ + not test_client_id_and_secret and \ + not test_setter_methods and \ + not test_retrieve_cr_token_success and \ + not test_get_authenticator_from_credential_file and \ + not test_get_authenticator_from_credential_file_scope and \ + not test_read_external_sources_2 and \ + not test_files_list\ + " +done diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/upstream/metadata python-ibm-cloud-sdk-core-3.12.0/debian/upstream/metadata --- python-ibm-cloud-sdk-core-1.6.2/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/upstream/metadata 2021-11-06 23:14:19.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/IBM/python-sdk-core/issues +Bug-Submit: https://github.com/IBM/python-sdk-core/issues/new +Repository: https://github.com/IBM/python-sdk-core.git +Repository-Browse: https://github.com/IBM/python-sdk-core diff -Nru python-ibm-cloud-sdk-core-1.6.2/debian/watch python-ibm-cloud-sdk-core-3.12.0/debian/watch --- python-ibm-cloud-sdk-core-1.6.2/debian/watch 2020-02-24 20:39:02.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/debian/watch 2021-11-06 23:14:19.000000000 +0000 @@ -1,3 +1,3 @@ -version=3 +version=4 opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ https://pypi.debian.net/ibm-cloud-sdk-core/ibm-cloud-sdk-core-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/api_exception.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/api_exception.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/api_exception.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/api_exception.py 2021-10-15 20:30:50.000000000 +0000 @@ -13,7 +13,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from http import HTTPStatus from typing import Optional + from requests import Response @@ -32,9 +35,9 @@ global_transaction_id (str, optional): Globally unique id the service endpoint has given a transaction. """ - def __init__(self, code: int, message: Optional[str] = None, http_response: Optional[Response] = None): + def __init__(self, code: int, *, message: Optional[str] = None, http_response: Optional[Response] = None) -> None: # Call the base class constructor with the parameters it needs - super(ApiException, self).__init__(message) + super().__init__(message) self.message = message self.code = code self.http_response = http_response @@ -43,14 +46,14 @@ self.global_transaction_id = http_response.headers.get('X-Global-Transaction-ID') self.message = self.message if self.message else self._get_error_message(http_response) - def __str__(self): + def __str__(self) -> str: msg = 'Error: ' + str(self.message) + ', Code: ' + str(self.code) if self.global_transaction_id is not None: msg += ' , X-global-transaction-id: ' + str(self.global_transaction_id) return msg @staticmethod - def _get_error_message(response: Response): + def _get_error_message(response: Response) -> str: error_message = 'Unknown error' try: error_json = response.json() @@ -64,6 +67,10 @@ error_message = error_json['message'] elif 'errorMessage' in error_json: error_message = error_json['errorMessage'] + elif response.status_code == 401: + error_message = 'Unauthorized: Access is denied due to invalid credentials' + else: + error_message = HTTPStatus(response.status_code).phrase return error_message except: return response.text or error_message diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -16,20 +16,42 @@ from abc import ABC, abstractmethod + class Authenticator(ABC): """This interface defines the common methods and constants associated with an Authenticator implementation.""" + + # Constants representing the various authenticator types. + AUTHTYPE_BASIC = 'basic' + AUTHTYPE_BEARERTOKEN = 'bearerToken' + AUTHTYPE_IAM = 'iam' + AUTHTYPE_CONTAINER = 'container' + AUTHTYPE_CP4D = 'cp4d' + AUTHTYPE_NOAUTH = 'noAuth' + AUTHTYPE_UNKNOWN = 'unknown' + @abstractmethod - def authenticate(self, req: dict): + def authenticate(self, req: dict) -> None: """Perform the necessary authentication steps for the specified request. + Attributes: + req (dict): Will be modified to contain the appropriate authentication information. + To be implemented by subclasses. """ pass @abstractmethod - def validate(self): + def validate(self) -> None: """Validates the current set of configuration information in the Authenticator. + Raises: + ValueError: The configuration information is not valid for service operations. + To be implemented by subclasses. """ pass + + # pylint: disable=R0201 + def authentication_type(self) -> str: + """Returns the authenticator's type. This method should be overridden by each authenticator implementation.""" + return Authenticator.AUTHTYPE_UNKNOWN diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/basic_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/basic_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/basic_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/basic_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -35,16 +35,18 @@ Raises: ValueError: The username or password is not specified or contains invalid characters. """ - authentication_type = 'basic' - def __init__(self, username: str, password: str): + def __init__(self, username: str, password: str) -> None: self.username = username self.password = password self.validate() self.authorization_header = self.__construct_basic_auth_header() + def authentication_type(self) -> str: + """Returns this authenticator's type ('basic').""" + return Authenticator.AUTHTYPE_BASIC - def validate(self): + def validate(self) -> None: """Validate username and password. Ensure the username and password are valid for service operations. @@ -61,14 +63,13 @@ 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.') - - def __construct_basic_auth_header(self): + def __construct_basic_auth_header(self) -> str: authstring = "{0}:{1}".format(self.username, self.password) - base64_authorization = base64.b64encode(authstring.encode('utf-8')).decode('utf-8') + base64_authorization = base64.b64encode( + authstring.encode('utf-8')).decode('utf-8') return 'Basic {0}'.format(base64_authorization) - - def authenticate(self, req: Request): + def authenticate(self, req: Request) -> None: """Add basic authentication information to a request. Basic Authorization will be added to the request's headers in the form: diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -18,6 +18,7 @@ from .authenticator import Authenticator + class BearerTokenAuthenticator(Authenticator): """The BearerTokenAuthenticator will add a user-supplied bearer token to requests. @@ -32,13 +33,16 @@ Raises: ValueError: Bearer token is none. """ - authentication_type = 'bearerToken' - def __init__(self, bearer_token: str): + def __init__(self, bearer_token: str) -> None: self.bearer_token = bearer_token self.validate() - def validate(self): + def authentication_type(self) -> str: + """Returns this authenticator's type ('bearertoken').""" + return Authenticator.AUTHTYPE_BEARERTOKEN + + def validate(self) -> None: """Validate the bearer token. Ensures the bearer token is valid for service operations. @@ -49,7 +53,7 @@ if self.bearer_token is None: raise ValueError('The bearer token shouldn\'t be None.') - def authenticate(self, req: Request): + def authenticate(self, req: Request) -> None: """Adds bearer authentication information to the request. The bearer token will be added to the request's headers in the form: @@ -63,7 +67,7 @@ headers = req.get('headers') headers['Authorization'] = 'Bearer {0}'.format(self.bearer_token) - def set_bearer_token(self, bearer_token: str): + def set_bearer_token(self, bearer_token: str) -> None: """Set a new bearer token to be sent in subsequent service operations. Args: diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/container_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/container_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/container_authenticator.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/container_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,141 @@ +# coding: utf-8 + +# Copyright 2021 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Optional + +from .iam_request_based_authenticator import IAMRequestBasedAuthenticator +from ..token_managers.container_token_manager import ContainerTokenManager +from .authenticator import Authenticator + + +class ContainerAuthenticator(IAMRequestBasedAuthenticator): + """ContainerAuthenticator implements an IAM-based authentication schema where by it + retrieves a "compute resource token" from the local compute resource (VM) + and uses that to obtain an IAM access token by invoking the IAM "get token" operation with grant-type=cr-token. + The resulting IAM access token is then added to outbound requests in an Authorization header of the form: + + Authorization: Bearer + + Args: + cr_token_filename: The name of the file containing the injected CR token value + (applies to IKS-managed compute resources). Defaults to "/var/run/secrets/tokens/vault-token". + iam_profile_name: The name of the linked trusted IAM profile to be used when obtaining the IAM access token + (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_profile_id must be specified. + Defaults to None. + iam_profile_id: The id of the linked trusted IAM profile to be used when obtaining the IAM access token + (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_profile_id must be specified. + Defaults to None. + url: The URL representing the IAM token service endpoint. If not specified, a suitable default value is used. + client_id: The client_id and client_secret fields are used to form + a "basic" authorization header for IAM token requests. Defaults to None. + client_secret: The client_id and client_secret fields are used to form + a "basic" authorization header for IAM token requests. Defaults to None. + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + headers: Default headers to be sent with every IAM token request. Defaults to None. + proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None. + proxies.http (optional): The proxy endpoint to use for HTTP requests. + proxies.https (optional): The proxy endpoint to use for HTTPS requests. + scope: The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + + Attributes: + token_manager (ContainerTokenManager): Retrieves and manages IAM tokens + from the endpoint specified by the url. + + Raises: + TypeError: The `disable_ssl_verification` is not a bool. + ValueError: Neither of iam_profile_name or iam_profile_idk are set, + or client_id, and/or client_secret are not valid for IAM token requests. + """ + + def __init__(self, + cr_token_filename: Optional[str] = None, + iam_profile_name: Optional[str] = None, + iam_profile_id: Optional[str] = None, + url: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + disable_ssl_verification: bool = False, + scope: Optional[str] = None, + proxies: Optional[Dict[str, str]] = None, + headers: Optional[Dict[str, str]] = None) -> None: + # Check the type of `disable_ssl_verification`. Must be a bool. + if not isinstance(disable_ssl_verification, bool): + raise TypeError('disable_ssl_verification must be a bool') + + self.token_manager = ContainerTokenManager( + cr_token_filename=cr_token_filename, iam_profile_name=iam_profile_name, iam_profile_id=iam_profile_id, + url=url, client_id=client_id, client_secret=client_secret, + disable_ssl_verification=disable_ssl_verification, scope=scope, proxies=proxies, headers=headers) + + self.validate() + + def authentication_type(self) -> str: + """Returns this authenticator's type ('container').""" + return Authenticator.AUTHTYPE_CONTAINER + + def validate(self) -> None: + """Validates the iam_profile_name, iam_profile_id, client_id, and client_secret for IAM token requests. + + Ensure that one of the iam_profile_name or iam_profile_id are specified. Additionally, ensure + both of the client_id and client_secret are set if either of them are defined. + + Raises: + ValueError: Neither of iam_profile_name or iam_profile_idk are set, + or client_id, and/or client_secret are not valid for IAM token requests. + """ + super().validate() + + if not self.token_manager.iam_profile_name and not self.token_manager.iam_profile_id: + raise ValueError( + 'At least one of iam_profile_name or iam_profile_id must be specified.') + + def set_cr_token_filename(self, cr_token_filename: str) -> None: + """Set the location of the compute resource token on the local filesystem. + + Args: + cr_token_filename: path to the compute resource token + """ + self.token_manager.cr_token_filename = cr_token_filename + + def set_iam_profile_name(self, iam_profile_name: str) -> None: + """Set the name of the IAM profile. + + Args: + iam_profile_name: name of the linked trusted IAM profile to be used when obtaining the IAM access token + + Raises: + ValueError: Neither of iam_profile_name or iam_profile_idk are set, + or client_id, and/or client_secret are not valid for IAM token requests. + """ + self.token_manager.iam_profile_name = iam_profile_name + self.validate() + + def set_iam_profile_id(self, iam_profile_id: str) -> None: + """Set the id of the IAM profile. + + Args: + iam_profile_id: id of the linked trusted IAM profile to be used when obtaining the IAM access token + + Raises: + ValueError: Neither of iam_profile_name or iam_profile_idk are set, + or client_id, and/or client_secret are not valid for IAM token requests. + """ + self.token_manager.iam_profile_id = iam_profile_id + self.validate() diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -19,7 +19,7 @@ from requests import Request from .authenticator import Authenticator -from ..cp4d_token_manager import CP4DTokenManager +from ..token_managers.cp4d_token_manager import CP4DTokenManager from ..utils import has_bad_first_or_last_char @@ -31,12 +31,11 @@ Authorization: Bearer - Args: - username: The username used to obtain a bearer token. - password: The password used to obtain a bearer token. - url: The URL representing the Cloud Pak for Data token service endpoint. - Keyword Args: + username: The username used to obtain a bearer token [required]. + password: The password used to obtain a bearer token [required if apikey not specified]. + url: The URL representing the Cloud Pak for Data token service endpoint [required]. + apikey: The API key used to obtain a bearer token [required if password not specified]. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every CP4D token request. Defaults to None. @@ -45,25 +44,37 @@ proxies.https (optional): The proxy endpoint to use for HTTPS requests. Attributes: - token_manager (CP4DTokenManager): Retrives and manages CP4D tokens from the endpoint specified by the url. + token_manager (CP4DTokenManager): Retrieves and manages CP4D tokens from the endpoint specified by the url. Raises: - ValueError: The username, password, and/or url are not valid for CP4D token requests. + TypeError: The `disable_ssl_verification` is not a bool. + ValueError: The username, password/apikey, and/or url are not valid for CP4D token requests. """ - authenticationdict = 'cp4d' def __init__(self, - username: str, - password: str, - url: str, + username: str = None, + password: str = None, + url: str = None, + *, + apikey: str = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, - proxies: Optional[Dict[str, str]] = None): + proxies: Optional[Dict[str, str]] = None) -> None: + # Check the type of `disable_ssl_verification`. Must be a bool. + if not isinstance(disable_ssl_verification, bool): + raise TypeError('disable_ssl_verification must be a bool') + self.token_manager = CP4DTokenManager( - username, password, url, disable_ssl_verification, headers, proxies) + username=username, password=password, apikey=apikey, url=url, + disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies) + self.validate() - def validate(self): + def authentication_type(self) -> str: + """Returns this authenticator's type ('cp4d').""" + return Authenticator.AUTHTYPE_CP4D + + def validate(self) -> None: """Validate username, password, and url for token requests. Ensures the username, password, and url are not None. Additionally, ensures they do not contain invalid @@ -72,8 +83,13 @@ Raises: ValueError: The username, password, and/or url are not valid for token requests. """ - if self.token_manager.username is None or self.token_manager.password is None: - raise ValueError('The username and password shouldn\'t be None.') + if self.token_manager.username is None: + raise ValueError('The username shouldn\'t be None.') + + if ((self.token_manager.password is None and self.token_manager.apikey is None) + or (self.token_manager.password is not None and self.token_manager.apikey is not None)): + raise ValueError( + 'Exactly one of `apikey` or `password` must be specified.') if self.token_manager.url is None: raise ValueError('The url shouldn\'t be None.') @@ -89,7 +105,7 @@ 'The url shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.') - def authenticate(self, req: Request): + def authenticate(self, req: Request) -> None: """Adds CP4D authentication information to the request. The CP4D bearer token will be added to the request's headers in the form: @@ -104,16 +120,19 @@ bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) - def set_disable_ssl_verification(self, status: bool = False): + def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. Args: status: Set to true in order to disable SSL certificate verification. Defaults to False. + + Raises: + TypeError: The `status` is not a bool. """ self.token_manager.set_disable_ssl_verification(status) - def set_headers(self, headers: Dict[str, str]): + def set_headers(self, headers: Dict[str, str]) -> None: """Default headers to be sent with every CP4D token request. Args: @@ -121,7 +140,7 @@ """ self.token_manager.set_headers(headers) - def set_proxies(self, proxies: Dict[str, str]): + def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with CP4D on behalf of the host. Args: diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/iam_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/iam_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/iam_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/iam_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -16,13 +16,13 @@ from typing import Dict, Optional -from requests import Request - from .authenticator import Authenticator -from ..iam_token_manager import IAMTokenManager +from .iam_request_based_authenticator import IAMRequestBasedAuthenticator +from ..token_managers.iam_token_manager import IAMTokenManager from ..utils import has_bad_first_or_last_char -class IAMAuthenticator(Authenticator): + +class IAMAuthenticator(IAMRequestBasedAuthenticator): """The IAMAuthenticator utilizes an apikey, or client_id and client_secret pair to obtain a suitable bearer token, and adds it to requests. @@ -45,29 +45,43 @@ proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. + scope: The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. Attributes: - token_manager (IAMTokenManager): Retrives and manages IAM tokens from the endpoint specified by the url. + token_manager (IAMTokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. Raises: + TypeError: The `disable_ssl_verification` is not a bool. ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ - authentication_type = 'iam' def __init__(self, apikey: str, + *, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, - disable_ssl_verification: Optional[bool] = False, + disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, - proxies: Optional[Dict[str, str]] = None): + proxies: Optional[Dict[str, str]] = None, + scope: Optional[str] = None) -> None: + # Check the type of `disable_ssl_verification`. Must be a bool. + if not isinstance(disable_ssl_verification, bool): + raise TypeError('disable_ssl_verification must be a bool') + self.token_manager = IAMTokenManager( - apikey, url, client_id, client_secret, disable_ssl_verification, - headers, proxies) + apikey, url=url, client_id=client_id, client_secret=client_secret, + disable_ssl_verification=disable_ssl_verification, + headers=headers, proxies=proxies, scope=scope) + self.validate() - def validate(self): + def authentication_type(self) -> str: + """Returns this authenticator's type ('iam').""" + return Authenticator.AUTHTYPE_IAM + + def validate(self) -> None: """Validates the apikey, client_id, and client_secret for IAM token requests. Ensure the apikey is not none, and has no bad characters. Additionally, ensure the @@ -76,6 +90,8 @@ Raises: ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ + super().validate() + if self.token_manager.apikey is None: raise ValueError('The apikey shouldn\'t be None.') @@ -83,65 +99,3 @@ raise ValueError( 'The apikey shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.') - - if (self.token_manager.client_id and - not self.token_manager.client_secret) or ( - not self.token_manager.client_id and - self.token_manager.client_secret): - raise ValueError( - 'Both client_id and client_secret should be initialized.') - - def authenticate(self, req: Request): - """Adds IAM authentication information to the request. - - The IAM bearer token will be added to the request's headers in the form: - - Authorization: Bearer - - Args: - req: The request to add IAM authentication information too. Must contain a key to a dictionary - called headers. - """ - headers = req.get('headers') - bearer_token = self.token_manager.get_token() - headers['Authorization'] = 'Bearer {0}'.format(bearer_token) - - def set_client_id_and_secret(self, client_id: str, client_secret: str): - """Set the client_id and client_secret pair the token manager will use for IAM token requests. - - Args: - client_id: The client id to be used in basic auth. - client_secret: The client secret to be used in basic auth. - - Raises: - ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. - """ - self.token_manager.set_client_id_and_secret(client_id, client_secret) - self.validate() - - def set_disable_ssl_verification(self, status: bool = False): - """Set the flag that indicates whether verification of the server's SSL certificate should be - disabled or not. Defaults to False. - - Keyword Arguments: - status: Headers to be sent with every IAM token request. Defaults to None. - """ - self.token_manager.set_disable_ssl_verification(status) - - def set_headers(self, headers: Dict[str, str]): - """Headers to be sent with every IAM token request. - - Args: - headers: Headers to be sent with every IAM token request. - """ - self.token_manager.set_headers(headers) - - def set_proxies(self, proxies: Dict[str, str]): - """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. - - Args: - proxies: Dictionary for mapping request protocol to proxy URL. - proxies.http (optional): The proxy endpoint to use for HTTP requests. - proxies.https (optional): The proxy endpoint to use for HTTPS requests. - """ - self.token_manager.set_proxies(proxies) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,116 @@ +# coding: utf-8 + +# Copyright 2019 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict + +from requests import Request + +from .authenticator import Authenticator + + +class IAMRequestBasedAuthenticator(Authenticator): + """The IAMRequestBasedAuthenticator class contains code that is common to all authenticators + that need to interact with the IAM tokens service to obtain an access token. + + The bearer token will be sent as an Authorization header in the form: + + Authorization: Bearer + + Attributes: + token_manager (TokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. + """ + + def validate(self) -> None: + """Validates the client_id, and client_secret for IAM token requests. + + Ensure both the client_id and client_secret are set if either of them are defined. + + Raises: + ValueError: The client_id, and/or client_secret are not valid for IAM token requests. + """ + if (self.token_manager.client_id and + not self.token_manager.client_secret) or ( + not self.token_manager.client_id and + self.token_manager.client_secret): + raise ValueError( + 'Both client_id and client_secret should be initialized.') + + def authenticate(self, req: Request) -> None: + """Adds IAM authentication information to the request. + + The IAM bearer token will be added to the request's headers in the form: + + Authorization: Bearer + + Args: + req: The request to add IAM authentication information too. Must contain a key to a dictionary + called headers. + """ + headers = req.get('headers') + bearer_token = self.token_manager.get_token() + headers['Authorization'] = 'Bearer {0}'.format(bearer_token) + + def set_client_id_and_secret(self, client_id: str, client_secret: str) -> None: + """Set the client_id and client_secret pair the token manager will use for IAM token requests. + + Args: + client_id: The client id to be used in basic auth. + client_secret: The client secret to be used in basic auth. + + Raises: + ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. + """ + self.token_manager.set_client_id_and_secret(client_id, client_secret) + self.validate() + + def set_disable_ssl_verification(self, status: bool = False) -> None: + """Set the flag that indicates whether verification of the server's SSL certificate should be + disabled or not. Defaults to False. + + Args: + status: Headers to be sent with every IAM token request. Defaults to None + + Raises: + TypeError: The `status` is not a bool. + """ + self.token_manager.set_disable_ssl_verification(status) + + def set_headers(self, headers: Dict[str, str]) -> None: + """Headers to be sent with every IAM token request. + + Args: + headers: Headers to be sent with every IAM token request. + """ + self.token_manager.set_headers(headers) + + def set_proxies(self, proxies: Dict[str, str]) -> None: + """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. + + Args: + proxies: Dictionary for mapping request protocol to proxy URL. + proxies.http (optional): The proxy endpoint to use for HTTP requests. + proxies.https (optional): The proxy endpoint to use for HTTPS requests. + """ + self.token_manager.set_proxies(proxies) + + def set_scope(self, value: str) -> None: + """Sets the "scope" parameter to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + + Args: + value: A space seperated string that makes up the scope parameter. + """ + self.token_manager.set_scope(value) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/__init__.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/__init__.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/__init__.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/__init__.py 2021-10-15 20:30:50.000000000 +0000 @@ -36,6 +36,7 @@ from .authenticator import Authenticator from .basic_authenticator import BasicAuthenticator from .bearer_token_authenticator import BearerTokenAuthenticator +from .container_authenticator import ContainerAuthenticator from .cp4d_authenticator import CloudPakForDataAuthenticator from .iam_authenticator import IAMAuthenticator from .no_auth_authenticator import NoAuthAuthenticator diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -16,12 +16,16 @@ from .authenticator import Authenticator + class NoAuthAuthenticator(Authenticator): """Performs no authentication.""" - authentication_type = 'noAuth' - def validate(self): + def authentication_type(self) -> str: + """Returns this authenticator's type ('noauth').""" + return Authenticator.AUTHTYPE_NOAUTH + + def validate(self) -> None: pass - def authenticate(self, req): + def authenticate(self, req) -> None: pass diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/base_service.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/base_service.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/base_service.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/base_service.py 2021-10-15 20:30:50.000000000 +0000 @@ -14,27 +14,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +import gzip +import json as json_import import logging +import platform from http.cookiejar import CookieJar -import json as json_import from os.path import basename -import platform -import sys from typing import Dict, List, Optional, Tuple, Union +from urllib3.util.retry import Retry import requests +from requests.adapters import HTTPAdapter from requests.structures import CaseInsensitiveDict + from ibm_cloud_sdk_core.authenticators import Authenticator -from .version import __version__ -from .utils import has_bad_first_or_last_char, remove_null_values, cleanup_values, read_external_sources -from .detailed_response import DetailedResponse from .api_exception import ApiException -from .jwt_token_manager import JWTTokenManager +from .detailed_response import DetailedResponse +from .token_managers.token_manager import TokenManager +from .utils import (has_bad_first_or_last_char, remove_null_values, + cleanup_values, read_external_sources, strip_extra_slashes) +from .version import __version__ # Uncomment this to enable http debugging # import http.client as http_client # http_client.HTTPConnection.debuglevel = 1 + +#pylint: disable=too-many-instance-attributes +#pylint: disable=too-many-locals class BaseService: """Common functionality shared by generated service classes. @@ -46,6 +53,7 @@ authenticator: Adds authentication data to service requests. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. + enable_gzip_compression: A flag that indicates whether to enable gzip compression on request bodies Attributes: service_url (str): Url to the service endpoint. @@ -55,6 +63,9 @@ default_headers (dict): A dictionary of headers to be sent with every HTTP request to the service endpoint. jar (http.cookiejar.CookieJar): Stores cookies received from the service. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. + http_client (Session): A configurable session which can use Transport Adapters to configure retries, timeouts, + proxies, etc. globally for all requests. + enable_gzip_compression (bool): A flag that indicates whether to enable gzip compression on request bodies Raises: ValueError: If Authenticator is not provided or invalid type. """ @@ -65,35 +76,72 @@ 'disable_ssl_verification option of the authenticator.' def __init__(self, + *, service_url: str = None, authenticator: Authenticator = None, - disable_ssl_verification: bool = False): - self.service_url = service_url + disable_ssl_verification: bool = False, + enable_gzip_compression: bool = False) -> None: + self.set_service_url(service_url) + self.http_client = requests.Session() self.http_config = {} self.jar = CookieJar() self.authenticator = authenticator self.disable_ssl_verification = disable_ssl_verification self.default_headers = None + self.enable_gzip_compression = enable_gzip_compression self._set_user_agent_header(self._build_user_agent()) + self.retry_config = None + self.http_adapter = HTTPAdapter() if not self.authenticator: raise ValueError('authenticator must be provided') if not isinstance(self.authenticator, Authenticator): - raise ValueError( - 'authenticator should be of type Authenticator') + raise ValueError('authenticator should be of type Authenticator') + + def enable_retries(self, max_retries: int = 4, retry_interval: float = 1.0) -> None: + """Enable automatic retries on the underlying http client used by the BaseService instance. + + Args: + max_retries: the maximum number of retries to attempt for a failed retryable request + retry_interval: the default wait time (in seconds) to use for the first retry attempt. + In general, if a response includes the Retry-After header, that will be used for + the wait time associated with the retry attempt. If the Retry-After header is not + present, then the wait time is based on the retry_interval and retry attempt number: + wait_time = retry_interval * (2 ^ (n-1)), where n is the retry attempt number + """ + self.retry_config = Retry( + total=max_retries, + backoff_factor=retry_interval, + # List of HTTP status codes to retry on in addition to Timeout/Connection Errors + status_forcelist=[429, 500, 502, 503, 504], + # List of HTTP methods to retry on + # Omitting this will default to all methods except POST + allowed_methods=['HEAD', 'GET', 'PUT', + 'DELETE', 'OPTIONS', 'TRACE', 'POST'] + ) + self.http_adapter = HTTPAdapter(max_retries=self.retry_config) + self.http_client.mount('http://', self.http_adapter) + self.http_client.mount('https://', self.http_adapter) + + def disable_retries(self): + """Remove retry config from http_adapter""" + self.retry_config = None + self.http_adapter = HTTPAdapter() + self.http_client.mount('http://', self.http_adapter) + self.http_client.mount('https://', self.http_adapter) @staticmethod - def _get_system_info(): + def _get_system_info() -> str: return '{0} {1} {2}'.format( platform.system(), # OS platform.release(), # OS version platform.python_version() # Python version ) - def _build_user_agent(self): + def _build_user_agent(self) -> str: return '{0}-{1} {2}'.format(self.SDK_NAME, __version__, self._get_system_info()) - def configure_service(self, service_name: str): + def configure_service(self, service_name: str) -> None: """Look for external configuration of a service. Set service properties. Try to get config from external sources, with the following priority: @@ -115,13 +163,24 @@ self.set_service_url(config.get('URL')) if config.get('DISABLE_SSL'): self.set_disable_ssl_verification( - bool(config.get('DISABLE_SSL')) - ) + config.get('DISABLE_SSL').lower() == 'true') + if config.get('ENABLE_GZIP'): + self.set_enable_gzip_compression( + config.get('ENABLE_GZIP').lower() == 'true') + if config.get('ENABLE_RETRIES'): + if config.get('ENABLE_RETRIES').lower() == 'true': + kwargs = {} + if config.get('MAX_RETRIES'): + kwargs["max_retries"] = int(config.get('MAX_RETRIES')) + if config.get('RETRY_INTERVAL'): + kwargs["retry_interval"] = float( + config.get('RETRY_INTERVAL')) + self.enable_retries(**kwargs) - def _set_user_agent_header(self, user_agent_string=None): + def _set_user_agent_header(self, user_agent_string: str) -> None: self.user_agent_header = {'User-Agent': user_agent_string} - def set_http_config(self, http_config: dict): + def set_http_config(self, http_config: dict) -> None: """Sets the http config dictionary. The dictionary can contain values that control the timeout, proxies, and etc of HTTP requests. @@ -134,13 +193,15 @@ """ if isinstance(http_config, dict): self.http_config = http_config - if (self.authenticator and hasattr(self.authenticator, 'token_manager') and - isinstance(self.authenticator.token_manager, JWTTokenManager)): + if (self.authenticator + and hasattr(self.authenticator, 'token_manager') + and isinstance(self.authenticator.token_manager, + TokenManager)): self.authenticator.token_manager.http_config = http_config else: raise TypeError("http_config parameter must be a dictionary") - def set_disable_ssl_verification(self, status: bool = False): + def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. @@ -149,7 +210,7 @@ """ self.disable_ssl_verification = status - def set_service_url(self, service_url: str): + def set_service_url(self, service_url: str) -> None: """Set the url the service will make HTTP requests too. Arguments: @@ -165,6 +226,26 @@ ) self.service_url = service_url + def get_http_client(self) -> requests.sessions.Session: + """Get the http client session currently used by the service. + + Returns: + The http client session currently used by the service. + """ + return self.http_client + + def set_http_client(self, http_client: requests.sessions.Session) -> None: + """Set current http client session + + Arguments: + http_client: A new requests session client + """ + if isinstance(http_client, requests.sessions.Session): + self.http_client = http_client + else: + raise TypeError( + "http_client parameter must be a requests.sessions.Session") + def get_authenticator(self) -> Authenticator: """Get the authenticator currently used by the service. @@ -173,7 +254,7 @@ """ return self.authenticator - def set_default_headers(self, headers: Dict[str, str]): + def set_default_headers(self, headers: Dict[str, str]) -> None: """Set http headers to be sent in every request. Arguments: @@ -204,13 +285,26 @@ if self.disable_ssl_verification: kwargs['verify'] = False + # Check to see if the caller specified the 'stream' argument. + stream_response = kwargs.get('stream') or False + + # Remove the keys we set manually, don't let the user to overwrite these. + reserved_keys = ['method', 'url', 'headers', 'params', 'cookies'] + for key in reserved_keys: + if key in kwargs: + del kwargs[key] + logging.warning('"%s" has been removed from the request', key) try: - response = requests.request(**request, cookies=self.jar, **kwargs) + response = self.http_client.request(**request, + cookies=self.jar, + **kwargs) if 200 <= response.status_code <= 299: if response.status_code == 204 or request['method'] == 'HEAD': # There is no body content for a HEAD request or a 204 response result = None + elif stream_response: + result = response elif not response.text: result = None else: @@ -218,36 +312,36 @@ result = response.json() except: result = response - return DetailedResponse(result, response.headers, - response.status_code) + return DetailedResponse(response=result, + headers=response.headers, + status_code=response.status_code) - error_message = None - if response.status_code == 401: - error_message = 'Unauthorized: Access is denied due to ' \ - 'invalid credentials' - raise ApiException( - response.status_code, error_message, http_response=response) + raise ApiException(response.status_code, http_response=response) except requests.exceptions.SSLError: logging.exception(self.ERROR_MSG_DISABLE_SSL) raise - except ApiException as err: - logging.exception(err.message) - raise - except: - logging.exception('Error in service call') - raise + def set_enable_gzip_compression(self, + should_enable_compression: bool = False + ) -> None: + """Set value to enable gzip compression on request bodies""" + self.enable_gzip_compression = should_enable_compression + + def get_enable_gzip_compression(self) -> bool: + """Get value for enabling gzip compression on request bodies""" + return self.enable_gzip_compression def prepare_request(self, method: str, url: str, + *, headers: Optional[dict] = None, params: Optional[dict] = None, data: Optional[Union[str, dict]] = None, - files: Optional[Union[ - Dict[str, Tuple[str]], - List[Tuple[str, Tuple[str, ...]]] - ]] = None, + files: Optional[Union[Dict[str, Tuple[str]], + List[Tuple[str, + Tuple[str, + ...]]]]] = None, **kwargs) -> dict: """Build a dict that represents an HTTP service request. @@ -274,7 +368,7 @@ # validate the service url is set if not self.service_url: raise ValueError('The service_url is required') - request['url'] = self.service_url + url + request['url'] = strip_extra_slashes(self.service_url + url) headers = remove_null_values(headers) if headers else {} headers = cleanup_values(headers) @@ -289,17 +383,27 @@ params = cleanup_values(params) request['params'] = params - if sys.version_info >= (3, 0) and isinstance(data, str): + if isinstance(data, str): data = data.encode('utf-8') - - if data and isinstance(data, dict): + elif isinstance(data, dict) and data: data = remove_null_values(data) - headers.update({'content-type': 'application/json'}) - data = json_import.dumps(data) + if headers.get('content-type') is None: + headers.update({'content-type': 'application/json'}) + data = json_import.dumps(data).encode('utf-8') request['data'] = data self.authenticator.authenticate(request) + # Compress the request body if applicable + if (self.get_enable_gzip_compression() + and 'content-encoding' not in headers + and request['data'] is not None): + headers['content-encoding'] = 'gzip' + uncompressed_data = request['data'] + request_body = gzip.compress(uncompressed_data) + request['data'] = request_body + request['headers'] = headers + # Next, we need to process the 'files' argument to try to fill in # any missing filenames where possible. # 'files' can be a dictionary (i.e { '': ()} ) @@ -313,7 +417,8 @@ files = files.items() # Next, fill in any missing filenames from file tuples. for part_name, file_tuple in files: - if file_tuple and len(file_tuple) == 3 and file_tuple[0] is None: + if file_tuple and len( + file_tuple) == 3 and file_tuple[0] is None: file = file_tuple[1] if file and hasattr(file, 'name'): filename = basename(file.name) @@ -340,7 +445,7 @@ # pylint: disable=protected-access @staticmethod - def _convert_model(val): + def _convert_model(val: str) -> None: if isinstance(val, str): val = json_import.loads(val) if hasattr(val, "_to_dict"): @@ -348,11 +453,11 @@ return val @staticmethod - def _convert_list(val): + def _convert_list(val: list) -> None: if isinstance(val, list): return ",".join(val) return val @staticmethod - def _encode_path_vars(*args): + def _encode_path_vars(*args) -> None: return (requests.utils.quote(x, safe='') for x in args) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/cp4d_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/cp4d_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/cp4d_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/cp4d_token_manager.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -# coding: utf-8 - -# Copyright 2019 IBM All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Dict, Optional -from .jwt_token_manager import JWTTokenManager - - -class CP4DTokenManager(JWTTokenManager): - """Token Manager of CloudPak for data. - - The Token Manager performs basic auth with a username and password - to acquire JWT tokens. - - Args: - username: The username for authentication. - password: The password for authentication. - url: The endpoint for JWT token requests. - - Keyword Arguments: - disable_ssl_verification: Disable ssl verification. Defaults to False. - headers: Headers to be sent with every service token request. Defaults to None. - proxies: Proxies to use for making request. Defaults to None. - proxies.http (optional): The proxy endpoint to use for HTTP requests. - proxies.https (optional): The proxy endpoint to use for HTTPS requests. - - Attributes: - username (str): The username for authentication. - password (str): The password for authentication. - url (str): The endpoint for JWT token requests. - headers (dict): Headers to be sent with every service token request. - proxies (dict): Proxies to use for making token requests. - proxies.http (str): The proxy endpoint to use for HTTP requests. - proxies.https (str): The proxy endpoint to use for HTTPS requests. - """ - TOKEN_NAME = 'accessToken' - VALIDATE_AUTH_PATH = '/v1/preauth/validateAuth' - - def __init__(self, - username: str, - password: str, - url: str, - disable_ssl_verification: bool = False, - headers: Optional[Dict[str, str]] = None, - proxies: Optional[Dict[str, str]] = None): - self.username = username - self.password = password - if url and not self.VALIDATE_AUTH_PATH in url: - url = url + '/v1/preauth/validateAuth' - self.headers = headers - self.proxies = proxies - super(CP4DTokenManager, self).__init__(url, disable_ssl_verification, - self.TOKEN_NAME) - - def request_token(self) -> dict: - """Makes a request for a token. - """ - auth_tuple = (self.username, self.password) - - response = self._request( - method='GET', - headers=self.headers, - url=self.url, - auth_tuple=auth_tuple, - proxies=self.proxies) - return response - - def set_headers(self, headers: Dict[str, str]): - """Headers to be sent with every CP4D token request. - - Args: - headers: The headers to be sent with every CP4D token request. - """ - if isinstance(headers, dict): - self.headers = headers - else: - raise TypeError('headers must be a dictionary') - - def set_proxies(self, proxies: Dict[str, str]): - """Sets the proxies the token manager will use to communicate with CP4D on behalf of the host. - - Args: - proxies: Proxies to use for making request. Defaults to None. - proxies.http (optional): The proxy endpoint to use for HTTP requests. - proxies.https (optional): The proxy endpoint to use for HTTPS requests. - """ - if isinstance(proxies, dict): - self.proxies = proxies - else: - raise TypeError('proxies must be a dictionary') diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/detailed_response.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/detailed_response.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/detailed_response.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/detailed_response.py 2021-10-15 20:30:50.000000000 +0000 @@ -14,11 +14,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json from typing import Dict, Optional -import json import requests + class DetailedResponse: """Custom class for detailed response returned from APIs. @@ -34,9 +35,10 @@ """ def __init__(self, + *, response: Optional[requests.Response] = None, headers: Optional[Dict[str, str]] = None, - status_code: Optional[int] = None): + status_code: Optional[int] = None) -> None: self.result = response self.headers = headers self.status_code = status_code @@ -65,7 +67,7 @@ """ return self.status_code - def _to_dict(self): + def _to_dict(self) -> dict: _dict = {} if hasattr(self, 'result') and self.result is not None: _dict['result'] = self.result if isinstance(self.result, (dict, list)) else 'HTTP response' @@ -75,5 +77,5 @@ _dict['status_code'] = self.status_code return _dict - def __str__(self): + def __str__(self) -> str: return json.dumps(self._to_dict(), indent=4, default=lambda o: o.__dict__) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/get_authenticator.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/get_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/get_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/get_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -14,10 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .authenticators import (Authenticator, BasicAuthenticator, BearerTokenAuthenticator, +from .authenticators import (Authenticator, BasicAuthenticator, BearerTokenAuthenticator, ContainerAuthenticator, CloudPakForDataAuthenticator, IAMAuthenticator, NoAuthAuthenticator) from .utils import read_external_sources + def get_authenticator_from_environment(service_name: str) -> Authenticator: """Look for external configuration of authenticator. @@ -38,31 +39,59 @@ authenticator = __construct_authenticator(config) return authenticator -def __construct_authenticator(config): - auth_type = config.get('AUTH_TYPE').lower() if config.get('AUTH_TYPE') else 'iam' + +def __construct_authenticator(config: dict) -> Authenticator: + # Determine the authentication type if not specified explicitly. + if config.get('AUTH_TYPE'): + auth_type = config.get('AUTH_TYPE') + elif config.get('AUTHTYPE'): + auth_type = config.get('AUTHTYPE') + else: + # If authtype wasn't specified explicitly, then determine the default. + # If the APIKEY property is specified, then it should be IAM, otherwise Container Auth. + if config.get('APIKEY'): + auth_type = Authenticator.AUTHTYPE_IAM + else: + auth_type = Authenticator.AUTHTYPE_CONTAINER + + auth_type = auth_type.lower() authenticator = None - if auth_type == 'basic': + if auth_type == Authenticator.AUTHTYPE_BASIC.lower(): authenticator = BasicAuthenticator( username=config.get('USERNAME'), password=config.get('PASSWORD')) - elif auth_type == 'bearertoken': + elif auth_type == Authenticator.AUTHTYPE_BEARERTOKEN.lower(): authenticator = BearerTokenAuthenticator( bearer_token=config.get('BEARER_TOKEN')) - elif auth_type == 'cp4d': + elif auth_type == Authenticator.AUTHTYPE_CONTAINER.lower(): + authenticator = ContainerAuthenticator( + cr_token_filename=config.get('CR_TOKEN_FILENAME'), + iam_profile_name=config.get('IAM_PROFILE_NAME'), + iam_profile_id=config.get('IAM_PROFILE_ID'), + url=config.get('AUTH_URL'), + client_id=config.get('CLIENT_ID'), + client_secret=config.get('CLIENT_SECRET'), + disable_ssl_verification=config.get( + 'AUTH_DISABLE_SSL', 'false').lower() == 'true', + scope=config.get('SCOPE')) + elif auth_type == Authenticator.AUTHTYPE_CP4D.lower(): authenticator = CloudPakForDataAuthenticator( username=config.get('USERNAME'), password=config.get('PASSWORD'), url=config.get('AUTH_URL'), - disable_ssl_verification=config.get('AUTH_DISABLE_SSL')) - elif auth_type == 'iam' and config.get('APIKEY'): + apikey=config.get('APIKEY'), + disable_ssl_verification=config.get('AUTH_DISABLE_SSL', 'false').lower() == 'true') + elif auth_type == Authenticator.AUTHTYPE_IAM.lower() and config.get('APIKEY'): authenticator = IAMAuthenticator( apikey=config.get('APIKEY'), url=config.get('AUTH_URL'), client_id=config.get('CLIENT_ID'), client_secret=config.get('CLIENT_SECRET'), - disable_ssl_verification=config.get('AUTH_DISABLE_SSL')) - elif auth_type == 'noauth': + disable_ssl_verification=config.get( + 'AUTH_DISABLE_SSL', 'false').lower() == 'true', + scope=config.get('SCOPE')) + elif auth_type == Authenticator.AUTHTYPE_NOAUTH.lower(): authenticator = NoAuthAuthenticator() return authenticator diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/iam_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/iam_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/iam_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/iam_token_manager.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,149 +0,0 @@ -# coding: utf-8 - -# Copyright 2019 IBM All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Dict, Optional -from .jwt_token_manager import JWTTokenManager - -class IAMTokenManager(JWTTokenManager): - """The IAMTokenManager takes an api key and performs the necessary interactions with - the IAM token service to obtain and store a suitable bearer token. Additionally, the IAMTokenManager - will retrieve bearer tokens via basic auth using a supplied client_id and client_secret pair. - - If the current stored bearer token has expired a new bearer token will be retrieved. - - Attributes: - apikey: A generated API key from ibmcloud. - url (str): The IAM endpoint to token requests. - client_id (str): The client_id and client_secret fields are used to form - a "basic auth" Authorization header for interactions with the IAM token server. - client_secret (str): The client_id and client_secret fields are used to form - a "basic auth" Authorization header for interactions with the IAM token server. - headers (dict): Default headers to be sent with every IAM token request. - proxies (dict): Proxies to use for communicating with IAM. - proxies.http (str): The proxy endpoint to use for HTTP requests. - proxies.https (str): The proxy endpoint to use for HTTPS requests. - http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. - - Args: - apikey: A generated APIKey from ibmcloud. - - Keyword Args: - url: The IAM endpoint to token requests. Defaults to None. - client_id: The client_id and client_secret fields are used to form - a "basic auth" Authorization header for interactions with the IAM token server. - Defaults to None. - client_secret: The client_id and client_secret fields are used to form - a "basic auth" Authorization header for interactions with the IAM token server. - Defaults to None. - disable_ssl_verification: A flag that indicates whether verification of - the server's SSL certificate should be disabled or not. Defaults to False. - headers: Default headers to be sent with every IAM token request. Defaults to None. - proxies: Proxies to use for communicating with IAM. Defaults to None. - proxies.http: The proxy endpoint to use for HTTP requests. - proxies.https: The proxy endpoint to use for HTTPS requests. - """ - DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com/identity/token' - CONTENT_TYPE = 'application/x-www-form-urlencoded' - REQUEST_TOKEN_GRANT_TYPE = 'urn:ibm:params:oauth:grant-type:apikey' - REQUEST_TOKEN_RESPONSE_TYPE = 'cloud_iam' - TOKEN_NAME = 'access_token' - - def __init__(self, - apikey: str, - url: Optional[str] = None, - client_id: Optional[str] = None, - client_secret: Optional[str] = None, - disable_ssl_verification: Optional[str] = False, - headers: Optional[Dict[str, str]] = None, - proxies: Optional[Dict[str, str]] = None): - self.apikey = apikey - self.url = url if url else self.DEFAULT_IAM_URL - self.client_id = client_id - self.client_secret = client_secret - self.headers = headers - self.proxies = proxies - super(IAMTokenManager, self).__init__( - self.url, disable_ssl_verification, self.TOKEN_NAME) - - def request_token(self) -> dict: - """Request an IAM OAuth token given an API Key. - - If client_id and client_secret are specified use their values as a user and pass auth set - according to WHATWG url spec. - - Returns: - A dictionary containing the bearer token to be subsequently used service requests. - """ - headers = { - 'Content-type': self.CONTENT_TYPE, - 'Accept': 'application/json' - } - if self.headers is not None and isinstance(self.headers, dict): - headers.update(self.headers) - - data = { - 'grant_type': self.REQUEST_TOKEN_GRANT_TYPE, - 'apikey': self.apikey, - 'response_type': self.REQUEST_TOKEN_RESPONSE_TYPE - } - - auth_tuple = None - # If both the client_id and secret were specified by the user, then use them - if self.client_id and self.client_secret: - auth_tuple = (self.client_id, self.client_secret) - - response = self._request( - method='POST', - url=self.url, - headers=headers, - data=data, - auth_tuple=auth_tuple, - proxies=self.proxies) - return response - - def set_client_id_and_secret(self, client_id: str, client_secret: str): - """Set the client_id and client_secret. - - Args: - client_id: The client id to be used for token requests. - client_secret: The client secret to be used for token requests. - """ - self.client_id = client_id - self.client_secret = client_secret - - def set_headers(self, headers: Dict[str, str]): - """Headers to be sent with every CP4D token request. - - Args: - headers: Headers to be sent with every IAM token request. - """ - if isinstance(headers, dict): - self.headers = headers - else: - raise TypeError('headers must be a dictionary') - - def set_proxies(self, proxies: Dict[str, str]): - """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. - - Args: - proxies: Proxies to use for communicating with IAM. - proxies.http (str, optional): The proxy endpoint to use for HTTP requests. - proxies.https (str, optional): The proxy endpoint to use for HTTPS requests. - """ - if isinstance(proxies, dict): - self.proxies = proxies - else: - raise TypeError('proxies must be a dictionary') diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/__init__.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/__init__.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/__init__.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/__init__.py 2021-10-15 20:30:50.000000000 +0000 @@ -25,21 +25,27 @@ functions: datetime_to_string: Serializes a datetime to a string. string_to_datetime: De-serializes a string to a datetime. + datetime_to_string_list: Serializes a list of datetimes to a list of strings. + string_to_datetime_list: De-serializes a list of strings to a list of datetimes. date_to_string: Serializes a date to a string. string_to_date: De-serializes a string to a date. convert_model: Convert a model object into an equivalent dict. convert_list: Convert a list of strings into comma-separated string. + get_query_param: Return a query parameter value from a URL read_external_sources: Get config object from external sources. get_authenticator_from_environment: Get authenticator from external sources. """ from .base_service import BaseService from .detailed_response import DetailedResponse -from .iam_token_manager import IAMTokenManager -from .jwt_token_manager import JWTTokenManager -from .cp4d_token_manager import CP4DTokenManager +from .token_managers.iam_token_manager import IAMTokenManager +from .token_managers.jwt_token_manager import JWTTokenManager +from .token_managers.cp4d_token_manager import CP4DTokenManager +from .token_managers.container_token_manager import ContainerTokenManager from .api_exception import ApiException from .utils import datetime_to_string, string_to_datetime, read_external_sources +from .utils import datetime_to_string_list, string_to_datetime_list from .utils import date_to_string, string_to_date from .utils import convert_model, convert_list +from .utils import get_query_param from .get_authenticator import get_authenticator_from_environment diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/jwt_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/jwt_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/jwt_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/jwt_token_manager.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,166 +0,0 @@ -# coding: utf-8 - -# Copyright 2019 IBM All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Optional - -import jwt -import requests -from .api_exception import ApiException - - -class JWTTokenManager: - """An abstract class to contain functionality for parsing, storing, and requesting JWT tokens. - - get_token will retrieve a new token from the url in case the that there is no existing token, - or the previous token has expired. Child classes will implement request_token, which will do - the actual acquisition of a new token. - - Args: - url: The url to request tokens from. - - Keyword Args: - disable_ssl_verification: A flag that indicates whether verification of - the server's SSL certificate should be disabled or not. Defaults to False. - token_name: The key that maps to the token in the dictionary returned from request_token. Defaults to None. - - Attributes: - url (str): The url to request tokens from. - disable_ssl_verification (bool): A flag that indicates whether verification of - the server's SSL certificate should be disabled or not. - token_name (str): The key used of the token in the dict returned from request_token. - token_info (dict): The most token_response from request_token. - time_for_new_token (int): The time in epoch seconds when the current token within token_info will expire. - http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. - """ - - def __init__(self, url: str, disable_ssl_verification: bool = False, token_name: Optional[str] = None): - self.url = url - self.disable_ssl_verification = disable_ssl_verification - self.token_name = token_name - self.token_info = {} - self.time_for_new_token = None - self.http_config = {} - - def get_token(self) -> str: - """Get a token to be used for authentication. - - The source of the token is determined by the following logic: - 1. a) If this class does not yet have one, make a request for one - b) If this class token has expired, request a new one - 2. If this class is managing tokens and has a valid token stored, send it - - Returns: - str: A valid access token - """ - if not self.token_info or self._is_token_expired(): - token_response = self.request_token() - self._save_token_info(token_response) - - return self.token_info.get(self.token_name) - - def set_disable_ssl_verification(self, status: bool = False): - """Sets the ssl verification to enabled or disabled. - - Args: - status: the flag to be used for determining status. - """ - self.disable_ssl_verification = status - - def request_token(self): - """Should be overridden by child classes. - - Raises: - NotImplementedError: Thrown when called. - """ - raise NotImplementedError( - 'request_token MUST be overridden by a subclass of JWTTokenManager.' - ) - - @staticmethod - def _get_current_time(): - return int(time.time()) - - def _is_token_expired(self): - """ - Check if currently stored token is expired. - - Using a buffer to prevent the edge case of the - token expiring before the request could be made. - - The buffer will be a fraction of the total TTL. Using 80%. - - Returns - ------- - bool - If token expired or not - """ - if not self.time_for_new_token: - return True - - current_time = self._get_current_time() - return self.time_for_new_token < current_time - - def _save_token_info(self, token_response): - """ - Decode the access token and save the response from the JWT service to the object's state - - Parameters - ---------- - token_response : str - Response from token service - """ - access_token = token_response.get(self.token_name) - - # The time of expiration is found by decoding the JWT access token - decoded_response = jwt.decode(access_token, verify=False) - exp = decoded_response.get('exp') - iat = decoded_response.get('iat') - - # exp is the time of expire and iat is the time of token retrieval - time_to_live = exp - iat - expire_time = exp - fraction_of_ttl = 0.8 - self.time_for_new_token = expire_time - (time_to_live * - (1.0 - fraction_of_ttl)) - self.token_info = token_response - - def _request(self, - method, - url, - headers=None, - params=None, - data=None, - auth_tuple=None, - **kwargs) -> dict: - kwargs = dict({"timeout": 60}, **kwargs) - kwargs = dict(kwargs, **self.http_config) - - if self.disable_ssl_verification: - kwargs['verify'] = False - - response = requests.request( - method=method, - url=url, - headers=headers, - params=params, - data=data, - auth=auth_tuple, - **kwargs) - if 200 <= response.status_code <= 299: - return response.json() - - raise ApiException(response.status_code, http_response=response) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/container_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/container_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/container_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/container_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,167 @@ +# coding: utf-8 + +# Copyright 2021 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Dict, Optional + +from .iam_request_based_token_manager import IAMRequestBasedTokenManager + + +class ContainerTokenManager(IAMRequestBasedTokenManager): + """The ContainerTokenManager takes a compute resource token and performs the necessary interactions with + the IAM token service to obtain and store a suitable bearer token. Additionally, the ContainerTokenManager + will retrieve bearer tokens via basic auth using a supplied client_id and client_secret pair. + + If the current stored bearer token has expired a new bearer token will be retrieved. + + Attributes: + cr_token_filename(str): The name of the file containing the injected CR token value + (applies to IKS-managed compute resources). + iam_profile_name (str): The name of the linked trusted IAM profile to be used when obtaining the + IAM access token (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_profile_id must be specified. + iam_profile_id (str): The id of the linked trusted IAM profile to be used when obtaining the IAM access token + (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_profile_id must be specified. + url (str): The IAM endpoint to token requests. + client_id (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + client_secret (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + headers (dict): Default headers to be sent with every IAM token request. + proxies (dict): Proxies to use for communicating with IAM. + proxies.http (str): The proxy endpoint to use for HTTP requests. + proxies.https (str): The proxy endpoint to use for HTTPS requests. + http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. + scope (str): The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + + Keyword Args: + cr_token_filename: The name of the file containing the injected CR token value + (applies to IKS-managed compute resources). Defaults to "/var/run/secrets/tokens/vault-token". + iam_profile_name: The name of the linked trusted IAM profile to be used when obtaining the IAM access token + (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_profile_id must be specified. + Defaults to None. + iam_profile_id: The id of the linked trusted IAM profile to be used when obtaining the IAM access token + (a CR token might map to multiple IAM profiles). + One of iam_profile_name or iam_prfoile_id must be specified. + Defaults to None. + url: The IAM endpoint to token requests. Defaults to None. + client_id: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + client_secret: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + headers: Default headers to be sent with every IAM token request. Defaults to None. + proxies: Proxies to use for communicating with IAM. Defaults to None. + proxies.http: The proxy endpoint to use for HTTP requests. + proxies.https: The proxy endpoint to use for HTTPS requests. + scope: The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + """ + DEFAULT_CR_TOKEN_FILENAME = '/var/run/secrets/tokens/vault-token' + + def __init__(self, + cr_token_filename: Optional[str] = None, + iam_profile_name: Optional[str] = None, + iam_profile_id: Optional[str] = None, + url: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + disable_ssl_verification: bool = False, + scope: Optional[str] = None, + proxies: Optional[Dict[str, str]] = None, + headers: Optional[Dict[str, str]] = None) -> None: + super().__init__( + url=url, client_id=client_id, client_secret=client_secret, + disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope) + + self.cr_token_filename = cr_token_filename + self.iam_profile_name = iam_profile_name + self.iam_profile_id = iam_profile_id + + self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:cr-token' + + def retrieve_cr_token(self) -> str: + """Retrieves the CR token for the current compute resource by reading it from the local file system. + + Raises: + Exception: Cannot retrieve the compute resource token from. + + Returns: + A string which contains the compute resource token. + """ + cr_token_filename = self.cr_token_filename if self.cr_token_filename else self.DEFAULT_CR_TOKEN_FILENAME + + logging.debug('Attempting to read CR token from file: %s', + cr_token_filename) + + try: + with open(cr_token_filename, 'r', encoding='utf-8') as file: + cr_token = file.read() + return cr_token + # pylint: disable=broad-except + except Exception as ex: + raise Exception( + 'Unable to retrieve the CR token value from file {}: {}'.format(cr_token_filename, ex)) from None + + def request_token(self) -> dict: + """Retrieves a CR token value from the current compute resource, + then uses that to obtain a new IAM access token from the IAM token server. + + Returns: + A dictionary containing the bearer token to be subsequently used service requests. + """ + # Retrieve the CR token for this compute resource. + cr_token = self.retrieve_cr_token() + + # Set the request payload. + self.request_payload['cr_token'] = cr_token + + if self.iam_profile_id: + self.request_payload['profile_id'] = self.iam_profile_id + if self.iam_profile_name: + self.request_payload['profile_name'] = self.iam_profile_name + + return super().request_token() + + def set_cr_token_filename(self, cr_token_filename: str) -> None: + """Set the location of the compute resource token on the local filesystem. + + Args: + cr_token_filename: path to the compute resource token + """ + self.cr_token_filename = cr_token_filename + + def set_iam_profile_name(self, iam_profile_name: str) -> None: + """Set the name of the IAM profile. + + Args: + iam_profile_name: name of the linked trusted IAM profile to be used when obtaining the IAM access token + """ + self.iam_profile_name = iam_profile_name + + def set_iam_profile_id(self, iam_profile_id: str) -> None: + """Set the id of the IAM profile. + + Args: + iam_profile_id: id of the linked trusted IAM profile to be used when obtaining the IAM access token + """ + self.iam_profile_id = iam_profile_id diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,111 @@ +# coding: utf-8 + +# Copyright 2019 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from typing import Dict, Optional + +from .jwt_token_manager import JWTTokenManager + + +class CP4DTokenManager(JWTTokenManager): + """Token Manager of CloudPak for data. + + The Token Manager performs basic auth with a username and password + to acquire JWT tokens. + + Keyword Arguments: + username: The username for authentication [required]. + password: The password for authentication [required if apikey not specified]. + url: The endpoint for JWT token requests [required]. + apikey: The apikey for authentication [required if password not specified]. + disable_ssl_verification: Disable ssl verification. Defaults to False. + headers: Headers to be sent with every service token request. Defaults to None. + proxies: Proxies to use for making request. Defaults to None. + proxies.http (optional): The proxy endpoint to use for HTTP requests. + proxies.https (optional): The proxy endpoint to use for HTTPS requests. + + Attributes: + username (str): The username for authentication. + password (str): The password for authentication. + url (str): The endpoint for JWT token requests. + headers (dict): Headers to be sent with every service token request. + proxies (dict): Proxies to use for making token requests. + proxies.http (str): The proxy endpoint to use for HTTP requests. + proxies.https (str): The proxy endpoint to use for HTTPS requests. + """ + TOKEN_NAME = 'token' + VALIDATE_AUTH_PATH = '/v1/authorize' + + def __init__(self, + username: str = None, + password: str = None, + url: str = None, + *, + apikey: str = None, + disable_ssl_verification: bool = False, + headers: Optional[Dict[str, str]] = None, + proxies: Optional[Dict[str, str]] = None) -> None: + self.username = username + self.password = password + if url and not self.VALIDATE_AUTH_PATH in url: + url = url + '/v1/authorize' + self.apikey = apikey + self.headers = headers + if self.headers is None: + self.headers = {} + self.headers['Content-Type'] = 'application/json' + self.proxies = proxies + super().__init__(url, disable_ssl_verification=disable_ssl_verification, + token_name=self.TOKEN_NAME) + + def request_token(self) -> dict: + """Makes a request for a token. + """ + response = self._request( + method='POST', + headers=self.headers, + url=self.url, + data=json.dumps({ + "username": self.username, + "password": self.password, + "api_key": self.apikey + }), + proxies=self.proxies) + return response + + def set_headers(self, headers: Dict[str, str]) -> None: + """Headers to be sent with every CP4D token request. + + Args: + headers: The headers to be sent with every CP4D token request. + """ + if isinstance(headers, dict): + self.headers = headers + else: + raise TypeError('headers must be a dictionary') + + def set_proxies(self, proxies: Dict[str, str]) -> None: + """Sets the proxies the token manager will use to communicate with CP4D on behalf of the host. + + Args: + proxies: Proxies to use for making request. Defaults to None. + proxies.http (optional): The proxy endpoint to use for HTTP requests. + proxies.https (optional): The proxy endpoint to use for HTTPS requests. + """ + if isinstance(proxies, dict): + self.proxies = proxies + else: + raise TypeError('proxies must be a dictionary') diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,169 @@ +# coding: utf-8 + +# Copyright 2019 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Optional + +from .jwt_token_manager import JWTTokenManager + + +#pylint: disable=too-many-instance-attributes +class IAMRequestBasedTokenManager(JWTTokenManager): + """The IamRequestBasedTokenManager class contains code relevant to any token manager that + interacts with the IAM service to manage a token. It stores information relevant to all + IAM requests, such as the client ID and secret, and performs the token request with a set + of request options common to any IAM token management scheme. + + If the current stored bearer token has expired a new bearer token will be retrieved. + + Attributes: + request_payload(dict): the data that will be sent in the IAM OAuth token request + url (str): The IAM endpoint to token requests. + client_id (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + client_secret (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + headers (dict): Default headers to be sent with every IAM token request. + proxies (dict): Proxies to use for communicating with IAM. + proxies.http (str): The proxy endpoint to use for HTTP requests. + proxies.https (str): The proxy endpoint to use for HTTPS requests. + http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. + scope (str): The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + + Keyword Args: + url: The IAM endpoint to token requests. Defaults to None. + client_id: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + client_secret: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + headers: Default headers to be sent with every IAM token request. Defaults to None. + proxies: Proxies to use for communicating with IAM. Defaults to None. + proxies.http: The proxy endpoint to use for HTTP requests. + proxies.https: The proxy endpoint to use for HTTPS requests. + scope: The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + """ + DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com' + OPERATION_PATH = "/identity/token" + + def __init__(self, + url: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + disable_ssl_verification: bool = False, + headers: Optional[Dict[str, str]] = None, + proxies: Optional[Dict[str, str]] = None, + scope: Optional[str] = None) -> None: + if not url: + url = self.DEFAULT_IAM_URL + if url.endswith(self.OPERATION_PATH): + url = url[:-len(self.OPERATION_PATH)] + self.url = url + self.client_id = client_id + self.client_secret = client_secret + self.headers = headers + self.refresh_token = None + self.proxies = proxies + self.scope = scope + self.request_payload = {} + super().__init__( + self.url, disable_ssl_verification=disable_ssl_verification, token_name='access_token') + + def request_token(self) -> dict: + """Request an IAM OAuth token given an API Key. + + If client_id and client_secret are specified use their values as a user and pass auth set + according to WHATWG url spec. + + Returns: + A dictionary containing the bearer token to be subsequently used service requests. + """ + headers = { + 'Content-type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + } + if self.headers is not None and isinstance(self.headers, dict): + headers.update(self.headers) + + data = dict(self.request_payload) + + if self.scope is not None and self.scope: + data['scope'] = self.scope + + auth_tuple = None + # If both the client_id and secret were specified by the user, then use them + if self.client_id and self.client_secret: + auth_tuple = (self.client_id, self.client_secret) + + response = self._request( + method='POST', + url=(self.url + self.OPERATION_PATH) if self.url else self.url, + headers=headers, + data=data, + auth_tuple=auth_tuple, + proxies=self.proxies) + return response + + def set_client_id_and_secret(self, client_id: str, client_secret: str) -> None: + """Set the client_id and client_secret. + + Args: + client_id: The client id to be used for token requests. + client_secret: The client secret to be used for token requests. + """ + self.client_id = client_id + self.client_secret = client_secret + + def set_headers(self, headers: Dict[str, str]) -> None: + """Headers to be sent with every CP4D token request. + + Args: + headers: Headers to be sent with every IAM token request. + """ + if isinstance(headers, dict): + self.headers = headers + else: + raise TypeError('headers must be a dictionary') + + def _save_token_info(self, token_response: dict) -> None: + super()._save_token_info(token_response) + + self.refresh_token = token_response.get("refresh_token") + + def set_proxies(self, proxies: Dict[str, str]) -> None: + """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. + + Args: + proxies: Proxies to use for communicating with IAM. + proxies.http (str, optional): The proxy endpoint to use for HTTP requests. + proxies.https (str, optional): The proxy endpoint to use for HTTPS requests. + """ + if isinstance(proxies, dict): + self.proxies = proxies + else: + raise TypeError('proxies must be a dictionary') + + def set_scope(self, value: str) -> None: + """Sets the "scope" parameter to use when fetching the bearer token from the IAM token server. + + Args: + value: A space seperated string that makes up the scope parameter. + """ + self.scope = value diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/iam_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/iam_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/iam_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/iam_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,82 @@ +# coding: utf-8 + +# Copyright 2019 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Optional + +from .iam_request_based_token_manager import IAMRequestBasedTokenManager + + +class IAMTokenManager(IAMRequestBasedTokenManager): + """The IAMTokenManager takes an api key and performs the necessary interactions with + the IAM token service to obtain and store a suitable bearer token. Additionally, the IAMTokenManager + If the current stored bearer token has expired a new bearer token will be retrieved. + + Attributes: + apikey: A generated API key from ibmcloud. + url (str): The IAM endpoint to token requests. + client_id (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + client_secret (str): The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + headers (dict): Default headers to be sent with every IAM token request. + proxies (dict): Proxies to use for communicating with IAM. + proxies.http (str): The proxy endpoint to use for HTTP requests. + proxies.https (str): The proxy endpoint to use for HTTPS requests. + http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. + scope (str): The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + + Args: + apikey: A generated APIKey from ibmcloud. + + Keyword Args: + url: The IAM endpoint to token requests. Defaults to None. + client_id: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + client_secret: The client_id and client_secret fields are used to form + a "basic auth" Authorization header for interactions with the IAM token server. + Defaults to None. + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + headers: Default headers to be sent with every IAM token request. Defaults to None. + proxies: Proxies to use for communicating with IAM. Defaults to None. + proxies.http: The proxy endpoint to use for HTTP requests. + proxies.https: The proxy endpoint to use for HTTPS requests. + scope: The "scope" to use when fetching the bearer token from the IAM token server. + This can be used to obtain an access token with a specific scope. + """ + + def __init__(self, + apikey: str, + *, + url: Optional[str] = None, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, + disable_ssl_verification: bool = False, + headers: Optional[Dict[str, str]] = None, + proxies: Optional[Dict[str, str]] = None, + scope: Optional[str] = None) -> None: + super().__init__( + url=url, client_id=client_id, client_secret=client_secret, + disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope) + + self.apikey = apikey + + # Set API key related data. + self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:apikey' + self.request_payload['apikey'] = self.apikey + self.request_payload['response_type'] = 'cloud_iam' diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/__init__.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/__init__.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/__init__.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,15 @@ +# coding: utf-8 + +# Copyright 2021 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/jwt_token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/jwt_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/jwt_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/jwt_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,104 @@ +# coding: utf-8 + +# Copyright 2019, 2020 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC +from typing import Optional + +import jwt +import requests + +from .token_manager import TokenManager +from ..api_exception import ApiException + + +class JWTTokenManager(TokenManager, ABC): + """An abstract class to contain functionality for parsing, storing, and requesting JWT tokens. + + get_token will retrieve a new token from the url in case the that there is no existing token, + or the previous token has expired. Child classes will implement request_token, which will do + the actual acquisition of a new token. + + Args: + url: The url to request tokens from. + + Keyword Args: + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + token_name: The key that maps to the token in the dictionary returned from request_token. Defaults to None. + + Attributes: + url (str): The url to request tokens from. + disable_ssl_verification (bool): A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. + token_name (str): The key used of the token in the dict returned from request_token. + token_info (dict): The most token_response from request_token. + """ + + def __init__(self, url: str, *, disable_ssl_verification: bool = False, token_name: Optional[str] = None): + super().__init__(url, disable_ssl_verification=disable_ssl_verification) + self.token_name = token_name + self.token_info = {} + + def _save_token_info(self, token_response: dict) -> None: + """ + Decode the access token and save the response from the JWT service to the object's state + Refresh time is set to approximately 80% of the token's TTL to ensure that + the token refresh completes before the current token expires. + Parameters + ---------- + token_response : dict + Response from token service + """ + self.token_info = token_response + self.access_token = token_response.get(self.token_name) + + # The time of expiration is found by decoding the JWT access token + decoded_response = jwt.decode(self.access_token, algorithms=["RS256"], options={"verify_signature": False}) + # exp is the time of expire and iat is the time of token retrieval + exp = decoded_response.get('exp') + iat = decoded_response.get('iat') + + self.expire_time = exp + buffer = (exp - iat) * 0.2 + self.refresh_time = self.expire_time - buffer + + def _request(self, + method, + url, + *, + headers=None, + params=None, + data=None, + auth_tuple=None, + **kwargs) -> dict: + kwargs = dict({"timeout": 60}, **kwargs) + kwargs = dict(kwargs, **self.http_config) + + if self.disable_ssl_verification: + kwargs['verify'] = False + + response = requests.request( + method=method, + url=url, + headers=headers, + params=params, + data=data, + auth=auth_tuple, + **kwargs) + if 200 <= response.status_code <= 299: + return response.json() + + raise ApiException(response.status_code, http_response=response) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/token_manager.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/token_managers/token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/token_managers/token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,219 @@ +# coding: utf-8 + +# Copyright 2020 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import time +from abc import ABC, abstractmethod +from threading import Lock + +import requests + +from ..api_exception import ApiException + + +# pylint: disable=too-many-instance-attributes +class TokenManager(ABC): + """An abstract class to contain functionality for parsing, storing, and requesting tokens. + + get_token will retrieve a new token from the url in case the that there is no existing token, + or the previous token has expired. Child classes will implement request_token, which will do + the actual acquisition of a new token. + + Args: + url: The url to request tokens from. + + Keyword Args: + disable_ssl_verification: A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. Defaults to False. + + Attributes: + url (str): The url to request tokens from. + disable_ssl_verification (bool): A flag that indicates whether verification of + the server's SSL certificate should be disabled or not. + expire_time (int): The time in epoch seconds when the current stored token will expire. + refresh_time (int): The time in epoch seconds when the current stored token should be refreshed. + request_time (int): The time the last outstanding token request was issued + lock (Lock): Lock variable to serialize access to refresh/request times + http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. + access_token (str): The latest stored access token + """ + + def __init__( + self, + url: str, + *, + disable_ssl_verification: bool = False + ): + self.url = url + self.disable_ssl_verification = disable_ssl_verification + self.expire_time = 0 + self.refresh_time = 0 + self.request_time = 0 + self.lock = Lock() + self.http_config = {} + self.access_token = None + + def get_token(self) -> str: + """Get a token to be used for authentication. + + The source of the token is determined by the following logic: + 1. a) If the current token is expired (or never fetched), make a request for one + b) If the current token should be refreshed, issue a refresh request + 2. After any requests initiated above complete, return the stored token + + Returns: + str: A valid access token + """ + if self._is_token_expired(): + self.paced_request_token() + + if self._token_needs_refresh(): + token_response = self.request_token() + self._save_token_info(token_response) + + return self.access_token + + def set_disable_ssl_verification(self, status: bool = False) -> None: + """Sets the ssl verification to enabled or disabled. + + Args: + status: the flag to be used for determining status. + + Raises: + TypeError: The `status` is not a bool. + """ + if isinstance(status, bool): + self.disable_ssl_verification = status + else: + raise TypeError('status must be a bool') + + def paced_request_token(self) -> None: + """ + Paces requests to request_token. + + This method pseudo-serializes requests for an access_token + when the current token is expired (or has never been fetched). + The first caller into this method records its `request_time` and + then issues the token request. Subsequent callers will check the + `request_time` to see if a request is active (has been issued within + the past 60 seconds), and if so will sleep for a short time interval + (currently 0.5 seconds) before checking again. The check for an active + request and update of `request_time` are serailized by the `lock` + variable so that only one caller can become the active requester + with a 60 second interval. + + Threads that sleep waiting for the active request to complete will + eventually find a newly valid token and return, or 60 seconds will + elapse and a new thread will assume the role of the active request. + """ + while self._is_token_expired(): + current_time = self._get_current_time() + + with self.lock: + request_active = self.request_time > (current_time - 60) + if not request_active: + self.request_time = current_time + + if not request_active: + token_response = self.request_token() + self._save_token_info(token_response) + self.request_time = 0 + return + + # Sleep for 0.5 seconds before checking token again + time.sleep(0.5) + + @abstractmethod # pragma: no cover + def request_token(self) -> None: + """Should be overridden by child classes.""" + pass + + @staticmethod + def _get_current_time() -> int: + return int(time.time()) + + def _is_token_expired(self) -> bool: + """ + Check if currently stored token is expired. + + Returns + ------- + bool + True if token is expired; False otherwise + """ + current_time = self._get_current_time() + return self.expire_time < current_time + + def _token_needs_refresh(self) -> bool: + """ + Check if currently stored token needs refresh. + + Returns + ------- + bool + True if token needs refresh; False otherwise + """ + current_time = self._get_current_time() + + with self.lock: + needs_refresh = self.refresh_time < current_time + if needs_refresh: + self.refresh_time = current_time + 60 + + return needs_refresh + + @abstractmethod + def _save_token_info(self, token_response: dict) -> None: # pragma: no cover + """ + Decode the access token and save the response from the service to the object's state + + Refresh time is set to approximately 80% of the token's TTL to ensure that + the token refresh completes before the current token expires. + + Parameters + ---------- + token_response : dict + Response from token service + """ + pass + + def _request(self, + method, + url, + *, + headers=None, + params=None, + data=None, + auth_tuple=None, + **kwargs): + kwargs = dict({"timeout": 60}, **kwargs) + kwargs = dict(kwargs, **self.http_config) + + if self.disable_ssl_verification: + kwargs['verify'] = False + + response = requests.request( + method=method, + url=url, + headers=headers, + params=params, + data=data, + auth=auth_tuple, + **kwargs) + if 200 <= response.status_code <= 299: + return response + + raise ApiException(response.status_code, http_response=response) diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/utils.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/utils.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/utils.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/utils.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,6 +1,6 @@ # coding: utf-8 -# Copyright 2019 IBM All Rights Reserved. +# Copyright 2019, 2021 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,9 +19,11 @@ from os import getenv, environ, getcwd from os.path import isfile, join, expanduser from typing import List, Union +from urllib.parse import urlparse, parse_qs import dateutil.parser as date_parser + def has_bad_first_or_last_char(val: str) -> bool: """Returns true if a string starts with any of: {," ; or ends with any of: },". @@ -31,7 +33,9 @@ Returns: Whether or not the string starts or ends with bad characters. """ - return val is not None and (val.startswith('{') or val.startswith('"') or val.endswith('}') or val.endswith('"')) + return val is not None and (val.startswith('{') or val.startswith('"') + or val.endswith('}') or val.endswith('"')) + def remove_null_values(dictionary: dict) -> dict: """Create a new dictionary without keys mapped to null values. @@ -46,6 +50,7 @@ return {k: v for (k, v) in dictionary.items() if v is not None} return dictionary + def cleanup_values(dictionary: dict) -> dict: """Create a new dictionary with boolean values converted to strings. @@ -62,12 +67,21 @@ return {k: cleanup_value(v) for (k, v) in dictionary.items()} return dictionary + def cleanup_value(value: any) -> any: """Convert a boolean value to string.""" if isinstance(value, bool): return 'true' if value else 'false' return value + +def strip_extra_slashes(value: str) -> str: + """Combine multiple trailing slashes to a single slash""" + if value.endswith('//'): + return value.rstrip('/') + '/' + return value + + def datetime_to_string(val: datetime.datetime) -> str: """Convert a datetime object to string. @@ -87,6 +101,7 @@ return val.isoformat().replace('+00:00', 'Z') return val + def string_to_datetime(string: str) -> datetime.datetime: """De-serializes string to datetime. @@ -101,6 +116,43 @@ return val return val.replace(tzinfo=datetime.timezone.utc) + +def string_to_datetime_list(string_list: List[str]) -> List[datetime.datetime]: + """De-serializes each string in a list to a datetime. + + Args: + string_list: list of strings containing datetime in iso8601 format. + + Returns: + the de-serialized list of strings as a list of datetime objects. + """ + if not isinstance(string_list, list): + raise ValueError("Invalid argument type: " + str(type(string_list)) + + ". Argument string_list must be of type List[str]") + datetime_list = [] + for string_val in string_list: + datetime_list.append(string_to_datetime(string_val)) + return datetime_list + + +def datetime_to_string_list(datetime_list: List[datetime.datetime]) -> List[str]: + """Convert a list of datetime objects to a list of strings. + + Args: + datetime_list: The list of datetime objects. + + Returns: + list of datetimes serialized as strings in iso8601 format. + """ + if not isinstance(datetime_list, list): + raise ValueError("Invalid argument type: " + str(type(datetime_list)) + + ". Argument datetime_list must be of type List[datetime.datetime]") + string_list = [] + for datetime_val in datetime_list: + string_list.append(datetime_to_string(datetime_val)) + return string_list + + def date_to_string(val: datetime.date) -> str: """Convert a date object to string. @@ -114,6 +166,7 @@ return str(val) return val + def string_to_date(string: str) -> datetime.date: """De-serializes string to date. @@ -125,6 +178,32 @@ """ return date_parser.parse(string).date() + +def get_query_param(url_str: str, param: str) -> str: + """Return a query parameter value from url_str + + Args: + url_str: the URL from which to extract the query + parameter value + param: the name of the query parameter whose value + should be returned + + Returns: + the value of the `param` query parameter as a string + + Raises: + ValueError: if errors are encountered parsing `url_str` + """ + if not url_str: + return None + url = urlparse(url_str) + if not url.query: + return None + query = parse_qs(url.query, strict_parsing=True) + values = query.get(param) + return values[0] if values else None + + def convert_model(val: any) -> dict: """Convert a model object into an equivalent dict. @@ -141,6 +220,7 @@ # Consider raising a ValueError here in the next major release return val + def convert_list(val: Union[str, List[str]]) -> str: """Convert a list of strings into comma-separated string. @@ -157,6 +237,7 @@ # Consider raising a ValueError here in the next major release return val + def read_external_sources(service_name: str) -> dict: """Look for external configuration of a service. @@ -183,6 +264,7 @@ return config + def __read_from_env_variables(service_name: str) -> dict: """Return a config object based on environment variables for a service. @@ -197,7 +279,10 @@ _parse_key_and_update_config(config, service_name, key, value) return config -def __read_from_credential_file(service_name: str, separator: str = '=') -> dict: + +def __read_from_credential_file(service_name: str, + *, + separator: str = '=') -> dict: """Return a config object based on credentials file for a service. Args: @@ -211,17 +296,16 @@ """ default_credentials_file_name = 'ibm-credentials.env' - # File path specified by an env variable + # 1. ${IBM_CREDENTIALS_FILE} credential_file_path = getenv('IBM_CREDENTIALS_FILE') - # Current working directory + # 2. /ibm-credentials.env if credential_file_path is None: - file_path = join( - getcwd(), default_credentials_file_name) + file_path = join(getcwd(), default_credentials_file_name) if isfile(file_path): credential_file_path = file_path - # Home directory + # 3. /ibm-credentials.env if credential_file_path is None: file_path = join(expanduser('~'), default_credentials_file_name) if isfile(file_path): @@ -229,20 +313,29 @@ config = {} if credential_file_path is not None: - with open(credential_file_path, 'r') as fobj: - for line in fobj: - key_val = line.strip().split(separator) - if len(key_val) == 2: - key = key_val[0] - value = key_val[1] - _parse_key_and_update_config(config, service_name, key, value) + try: + with open(credential_file_path, 'r', encoding='utf-8') as fobj: + for line in fobj: + key_val = line.strip().split(separator, 1) + if len(key_val) == 2: + key = key_val[0] + value = key_val[1] + _parse_key_and_update_config(config, service_name, key, + value) + except OSError: + # just absorb the exception and make sure we return an empty response + config = {} + return config -def _parse_key_and_update_config(config, service_name, key, value): + +def _parse_key_and_update_config(config: dict, service_name: str, key: str, + value: str) -> None: service_name = service_name.replace(' ', '_').replace('-', '_').upper() if key.startswith(service_name): config[key[len(service_name) + 1:]] = value + def __read_from_vcap_services(service_name: str) -> dict: """Return a config object based on the vcap services environment variable. @@ -258,29 +351,39 @@ services = json_import.loads(vcap_services) for key in services.keys(): for i in range(len(services[key])): - if vcap_service_credentials and isinstance(vcap_service_credentials, dict): + if vcap_service_credentials and isinstance( + vcap_service_credentials, dict): break if services[key][i].get('name') == service_name: - vcap_service_credentials = services[key][i].get('credentials', {}) + vcap_service_credentials = services[key][i].get( + 'credentials', {}) if not vcap_service_credentials: if service_name in services.keys(): service = services.get(service_name) if service: - vcap_service_credentials = service[0].get('credentials', {}) + vcap_service_credentials = service[0].get( + 'credentials', {}) - if vcap_service_credentials and isinstance(vcap_service_credentials, dict): + if vcap_service_credentials and isinstance(vcap_service_credentials, + dict): new_vcap_creds = {} - if vcap_service_credentials.get('username') and vcap_service_credentials.get('password'): # cf + # cf + if vcap_service_credentials.get( + 'username') and vcap_service_credentials.get('password'): new_vcap_creds['AUTH_TYPE'] = 'basic' - new_vcap_creds['USERNAME'] = vcap_service_credentials.get('username') - new_vcap_creds['PASSWORD'] = vcap_service_credentials.get('password') + new_vcap_creds['USERNAME'] = vcap_service_credentials.get( + 'username') + new_vcap_creds['PASSWORD'] = vcap_service_credentials.get( + 'password') vcap_service_credentials = new_vcap_creds elif vcap_service_credentials.get('iam_apikey'): new_vcap_creds['AUTH_TYPE'] = 'iam' - new_vcap_creds['APIKEY'] = vcap_service_credentials.get('iam_apikey') + new_vcap_creds['APIKEY'] = vcap_service_credentials.get( + 'iam_apikey') vcap_service_credentials = new_vcap_creds elif vcap_service_credentials.get('apikey'): new_vcap_creds['AUTH_TYPE'] = 'iam' - new_vcap_creds['APIKEY'] = vcap_service_credentials.get('apikey') + new_vcap_creds['APIKEY'] = vcap_service_credentials.get( + 'apikey') vcap_service_credentials = new_vcap_creds return vcap_service_credentials diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/version.py python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/version.py --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core/version.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core/version.py 2021-10-15 20:30:50.000000000 +0000 @@ -1 +1 @@ -__version__ = '1.6.2' +__version__ = '3.12.0' diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/PKG-INFO python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/PKG-INFO --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/PKG-INFO 2020-02-13 21:45:10.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/PKG-INFO 2021-10-15 20:32:00.000000000 +0000 @@ -1,129 +1,111 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: ibm-cloud-sdk-core -Version: 1.6.2 -Summary: Client library for the IBM Cloud services +Version: 3.12.0 +Summary: Core library used by SDKs for IBM Cloud Services Home-page: https://github.com/IBM/python-sdk-core -Author: Erika Dsouza -Author-email: erika.dsouza@ibm.com +Author: IBM +Author-email: devxsdk@us.ibm.com License: Apache 2.0 -Description: |Build Status| |codecov| |Latest Stable Version| |CLA assistant| - - python-sdk-core - =============== - - This project contains the core functionality used by Python SDK's - generated by the IBM OpenAPI 3 SDK Generator (openapi-sdkgen). Python - code generated by openapi-sdkgen will depend on the function contained - in this project. - - Notice - ====== - - Support for Python versions 2.x and versions <= 3.4 is deprecated and - will be officially dropped in the next major release, which is expected - to be end of December, 2019. - - Installation - ------------ - - To install, use ``pip`` or ``easy_install``: - - .. code:: bash - - pip install --upgrade ibm-cloud-sdk-core - - or - - .. code:: bash - - easy_install --upgrade ibm-cloud-sdk-core - - Authentication - -------------- - - The python-sdk-core project supports the following types of - authentication: - Basic Authentication - Bearer Token - Identity and - Access Management (IAM) - Cloud Pak for Data - No Authentication - - For more information about the various authentication types and how to - use them with your services, click `here `__ - - Issues - ------ - - If you encounter an issue with this project, you are welcome to submit a - `bug report `__. Before - opening a new issue, please search for similar issues. It's possible - that someone has already reported it. - - Logging - ------- - - Enable logging - ~~~~~~~~~~~~~~ - - .. code:: python - - import logging - logging.basicConfig(level=logging.DEBUG) - - This would show output of the form: - - :: - - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iam.cloud.ibm.com:443 - DEBUG:urllib3.connectionpool:https://iam.cloud.ibm.com:443 "POST /identity/token HTTP/1.1" 200 1809 - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "POST /assistant/api/v1/workspaces?version=2018-07-10 HTTP/1.1" 201 None - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "GET /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10&export=true HTTP/1.1" 200 None - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "DELETE /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10 HTTP/1.1" 200 28 - - Low level request and response dump - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - To get low level information of the requests/ responses: - - .. code:: python - - from http.client import HTTPConnection - HTTPConnection.debuglevel = 1 - - Open source @ IBM - ----------------- - - Find more open source projects on the `IBM Github - Page `__ - - License - ------- - - This library is licensed under Apache 2.0. Full license text is - available in `LICENSE `__. - - Contributing - ------------ - - See `CONTRIBUTING.md `__. - - .. |Build Status| image:: https://travis-ci.com/IBM/python-sdk-core.svg?branch=master - :target: https://travis-ci.com/IBM/python-sdk-core - .. |codecov| image:: https://codecov.io/gh/IBM/python-sdk-core/branch/master/graph/badge.svg - :target: https://codecov.io/gh/IBM/python-sdk-core - .. |Latest Stable Version| image:: https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg - :target: https://pypi.python.org/pypi/ibm-cloud-sdk-core - .. |CLA assistant| image:: https://cla-assistant.io/readme/badge/ibm/python-sdk-core - :target: https://cla-assistant.io/ibm/python-sdk-core - -Keywords: watson,ibm,cloud +Keywords: watson,ibm,cloud,ibm cloud services Platform: UNKNOWN Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 -Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Description-Content-Type: text/markdown +License-File: LICENSE + +[![Build Status](https://app.travis-ci.com/IBM/python-sdk-core.svg?branch=main)](https://app.travis-ci.com/IBM/python-sdk-core) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ibm-cloud-sdk-core)](https://pypi.org/project/ibm-cloud-sdk-core/) +[![Latest Stable Version](https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg)](https://pypi.python.org/pypi/ibm-cloud-sdk-core) +[![CLA assistant](https://cla-assistant.io/readme/badge/ibm/python-sdk-core)](https://cla-assistant.io/ibm/python-sdk-core) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)[![codecov](https://codecov.io/gh/IBM/python-sdk-core/branch/main/graph/badge.svg)](https://codecov.io/gh/IBM/python-sdk-core) + +# IBM Python SDK Core Version 3.12.0 +This project contains core functionality required by Python code generated by the IBM Cloud OpenAPI SDK Generator +(openapi-sdkgen). + +# Python Version +The current minimum Python version supported is 3.6. + +## Installation + +To install, use `pip` or `easy_install`: + +```bash +pip install --upgrade ibm-cloud-sdk-core +``` + +or + +```bash +easy_install --upgrade ibm-cloud-sdk-core +``` + +## Authentication +The python-sdk-core project supports the following types of authentication: +- Basic Authentication +- Bearer Token +- Identity and Access Management (IAM) +- Cloud Pak for Data +- Container +- No Authentication + +For more information about the various authentication types and how to use them with your services, click [here](Authentication.md) + +## Issues + +If you encounter an issue with this project, you are welcome to submit a [bug report](https://github.com/IBM/python-sdk-core/issues). +Before opening a new issue, please search for similar issues. It's possible that someone has already reported it. + +## Logging + +### Enable logging + +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +This would show output of the form: +``` +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iam.cloud.ibm.com:443 +DEBUG:urllib3.connectionpool:https://iam.cloud.ibm.com:443 "POST /identity/token HTTP/1.1" 200 1809 +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "POST /assistant/api/v1/workspaces?version=2018-07-10 HTTP/1.1" 201 None +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "GET /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10&export=true HTTP/1.1" 200 None +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "DELETE /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10 HTTP/1.1" 200 28 +``` + +### Low level request and response dump +To get low level information of the requests/ responses: + +```python +from http.client import HTTPConnection +HTTPConnection.debuglevel = 1 +``` + +## Open source @ IBM + +Find more open source projects on the [IBM Github Page](http://github.com/IBM) + +## License + +This library is licensed under Apache 2.0. Full license text is +available in [LICENSE](LICENSE). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). + + diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/requires.txt python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/requires.txt --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/requires.txt 2020-02-13 21:45:10.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/requires.txt 2021-10-15 20:32:00.000000000 +0000 @@ -1,3 +1,3 @@ -requests<3.0,>=2.0 -python_dateutil>=2.5.3 -PyJWT>=1.7.1 +requests<3.0,>=2.20 +python_dateutil<3.0.0,>=2.5.3 +PyJWT<3.0.0,>=2.0.1 diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/SOURCES.txt python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/SOURCES.txt --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/SOURCES.txt 2020-02-13 21:45:10.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/SOURCES.txt 2021-10-15 20:32:00.000000000 +0000 @@ -1,13 +1,14 @@ +LICENSE +MANIFEST.in README.md +requirements-dev.txt +requirements.txt setup.py ibm_cloud_sdk_core/__init__.py ibm_cloud_sdk_core/api_exception.py ibm_cloud_sdk_core/base_service.py -ibm_cloud_sdk_core/cp4d_token_manager.py ibm_cloud_sdk_core/detailed_response.py ibm_cloud_sdk_core/get_authenticator.py -ibm_cloud_sdk_core/iam_token_manager.py -ibm_cloud_sdk_core/jwt_token_manager.py ibm_cloud_sdk_core/utils.py ibm_cloud_sdk_core/version.py ibm_cloud_sdk_core.egg-info/PKG-INFO @@ -20,14 +21,26 @@ ibm_cloud_sdk_core/authenticators/authenticator.py ibm_cloud_sdk_core/authenticators/basic_authenticator.py ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py +ibm_cloud_sdk_core/authenticators/container_authenticator.py ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py ibm_cloud_sdk_core/authenticators/iam_authenticator.py +ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py +ibm_cloud_sdk_core/token_managers/__init__.py +ibm_cloud_sdk_core/token_managers/container_token_manager.py +ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py +ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py +ibm_cloud_sdk_core/token_managers/iam_token_manager.py +ibm_cloud_sdk_core/token_managers/jwt_token_manager.py +ibm_cloud_sdk_core/token_managers/token_manager.py test/__init__.py test/test_api_exception.py +test/test_authenticator.py test/test_base_service.py test/test_basic_authenticator.py test/test_bearer_authenticator.py +test/test_container_authenticator.py +test/test_container_token_manager.py test/test_cp4d_authenticator.py test/test_cp4d_token_manager.py test/test_detailed_response.py @@ -35,4 +48,7 @@ test/test_iam_token_manager.py test/test_jwt_token_manager.py test/test_no_auth_authenticator.py -test/test_utils.py \ No newline at end of file +test/test_token_manager.py +test/test_utils.py +test_integration/__init__.py +test_integration/test_cp4d_authenticator_integration.py \ No newline at end of file diff -Nru python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/top_level.txt python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/top_level.txt --- python-ibm-cloud-sdk-core-1.6.2/ibm_cloud_sdk_core.egg-info/top_level.txt 2020-02-13 21:45:10.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/ibm_cloud_sdk_core.egg-info/top_level.txt 2021-10-15 20:32:00.000000000 +0000 @@ -1,2 +1,3 @@ ibm_cloud_sdk_core test +test_integration diff -Nru python-ibm-cloud-sdk-core-1.6.2/LICENSE python-ibm-cloud-sdk-core-3.12.0/LICENSE --- python-ibm-cloud-sdk-core-1.6.2/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/LICENSE 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -Nru python-ibm-cloud-sdk-core-1.6.2/MANIFEST.in python-ibm-cloud-sdk-core-3.12.0/MANIFEST.in --- python-ibm-cloud-sdk-core-1.6.2/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/MANIFEST.in 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,3 @@ +include requirements.txt +include requirements-dev.txt +include LICENSE diff -Nru python-ibm-cloud-sdk-core-1.6.2/PKG-INFO python-ibm-cloud-sdk-core-3.12.0/PKG-INFO --- python-ibm-cloud-sdk-core-1.6.2/PKG-INFO 2020-02-13 21:45:10.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/PKG-INFO 2021-10-15 20:32:00.000000000 +0000 @@ -1,129 +1,111 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: ibm-cloud-sdk-core -Version: 1.6.2 -Summary: Client library for the IBM Cloud services +Version: 3.12.0 +Summary: Core library used by SDKs for IBM Cloud Services Home-page: https://github.com/IBM/python-sdk-core -Author: Erika Dsouza -Author-email: erika.dsouza@ibm.com +Author: IBM +Author-email: devxsdk@us.ibm.com License: Apache 2.0 -Description: |Build Status| |codecov| |Latest Stable Version| |CLA assistant| - - python-sdk-core - =============== - - This project contains the core functionality used by Python SDK's - generated by the IBM OpenAPI 3 SDK Generator (openapi-sdkgen). Python - code generated by openapi-sdkgen will depend on the function contained - in this project. - - Notice - ====== - - Support for Python versions 2.x and versions <= 3.4 is deprecated and - will be officially dropped in the next major release, which is expected - to be end of December, 2019. - - Installation - ------------ - - To install, use ``pip`` or ``easy_install``: - - .. code:: bash - - pip install --upgrade ibm-cloud-sdk-core - - or - - .. code:: bash - - easy_install --upgrade ibm-cloud-sdk-core - - Authentication - -------------- - - The python-sdk-core project supports the following types of - authentication: - Basic Authentication - Bearer Token - Identity and - Access Management (IAM) - Cloud Pak for Data - No Authentication - - For more information about the various authentication types and how to - use them with your services, click `here `__ - - Issues - ------ - - If you encounter an issue with this project, you are welcome to submit a - `bug report `__. Before - opening a new issue, please search for similar issues. It's possible - that someone has already reported it. - - Logging - ------- - - Enable logging - ~~~~~~~~~~~~~~ - - .. code:: python - - import logging - logging.basicConfig(level=logging.DEBUG) - - This would show output of the form: - - :: - - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iam.cloud.ibm.com:443 - DEBUG:urllib3.connectionpool:https://iam.cloud.ibm.com:443 "POST /identity/token HTTP/1.1" 200 1809 - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "POST /assistant/api/v1/workspaces?version=2018-07-10 HTTP/1.1" 201 None - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "GET /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10&export=true HTTP/1.1" 200 None - DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 - DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "DELETE /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10 HTTP/1.1" 200 28 - - Low level request and response dump - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - To get low level information of the requests/ responses: - - .. code:: python - - from http.client import HTTPConnection - HTTPConnection.debuglevel = 1 - - Open source @ IBM - ----------------- - - Find more open source projects on the `IBM Github - Page `__ - - License - ------- - - This library is licensed under Apache 2.0. Full license text is - available in `LICENSE `__. - - Contributing - ------------ - - See `CONTRIBUTING.md `__. - - .. |Build Status| image:: https://travis-ci.com/IBM/python-sdk-core.svg?branch=master - :target: https://travis-ci.com/IBM/python-sdk-core - .. |codecov| image:: https://codecov.io/gh/IBM/python-sdk-core/branch/master/graph/badge.svg - :target: https://codecov.io/gh/IBM/python-sdk-core - .. |Latest Stable Version| image:: https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg - :target: https://pypi.python.org/pypi/ibm-cloud-sdk-core - .. |CLA assistant| image:: https://cla-assistant.io/readme/badge/ibm/python-sdk-core - :target: https://cla-assistant.io/ibm/python-sdk-core - -Keywords: watson,ibm,cloud +Keywords: watson,ibm,cloud,ibm cloud services Platform: UNKNOWN Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 -Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Description-Content-Type: text/markdown +License-File: LICENSE + +[![Build Status](https://app.travis-ci.com/IBM/python-sdk-core.svg?branch=main)](https://app.travis-ci.com/IBM/python-sdk-core) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ibm-cloud-sdk-core)](https://pypi.org/project/ibm-cloud-sdk-core/) +[![Latest Stable Version](https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg)](https://pypi.python.org/pypi/ibm-cloud-sdk-core) +[![CLA assistant](https://cla-assistant.io/readme/badge/ibm/python-sdk-core)](https://cla-assistant.io/ibm/python-sdk-core) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)[![codecov](https://codecov.io/gh/IBM/python-sdk-core/branch/main/graph/badge.svg)](https://codecov.io/gh/IBM/python-sdk-core) + +# IBM Python SDK Core Version 3.12.0 +This project contains core functionality required by Python code generated by the IBM Cloud OpenAPI SDK Generator +(openapi-sdkgen). + +# Python Version +The current minimum Python version supported is 3.6. + +## Installation + +To install, use `pip` or `easy_install`: + +```bash +pip install --upgrade ibm-cloud-sdk-core +``` + +or + +```bash +easy_install --upgrade ibm-cloud-sdk-core +``` + +## Authentication +The python-sdk-core project supports the following types of authentication: +- Basic Authentication +- Bearer Token +- Identity and Access Management (IAM) +- Cloud Pak for Data +- Container +- No Authentication + +For more information about the various authentication types and how to use them with your services, click [here](Authentication.md) + +## Issues + +If you encounter an issue with this project, you are welcome to submit a [bug report](https://github.com/IBM/python-sdk-core/issues). +Before opening a new issue, please search for similar issues. It's possible that someone has already reported it. + +## Logging + +### Enable logging + +```python +import logging +logging.basicConfig(level=logging.DEBUG) +``` + +This would show output of the form: +``` +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): iam.cloud.ibm.com:443 +DEBUG:urllib3.connectionpool:https://iam.cloud.ibm.com:443 "POST /identity/token HTTP/1.1" 200 1809 +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "POST /assistant/api/v1/workspaces?version=2018-07-10 HTTP/1.1" 201 None +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "GET /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10&export=true HTTP/1.1" 200 None +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): gateway.watsonplatform.net:443 +DEBUG:urllib3.connectionpool:https://gateway.watsonplatform.net:443 "DELETE /assistant/api/v1/workspaces/883a2a44-eb5f-4b1a-96b0-32a90b475ea8?version=2018-07-10 HTTP/1.1" 200 28 +``` + +### Low level request and response dump +To get low level information of the requests/ responses: + +```python +from http.client import HTTPConnection +HTTPConnection.debuglevel = 1 +``` + +## Open source @ IBM + +Find more open source projects on the [IBM Github Page](http://github.com/IBM) + +## License + +This library is licensed under Apache 2.0. Full license text is +available in [LICENSE](LICENSE). + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). + + diff -Nru python-ibm-cloud-sdk-core-1.6.2/README.md python-ibm-cloud-sdk-core-3.12.0/README.md --- python-ibm-cloud-sdk-core-1.6.2/README.md 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/README.md 2021-10-15 20:30:50.000000000 +0000 @@ -1,14 +1,15 @@ -[![Build Status](https://travis-ci.com/IBM/python-sdk-core.svg?branch=master)](https://travis-ci.com/IBM/python-sdk-core) -[![codecov](https://codecov.io/gh/IBM/python-sdk-core/branch/master/graph/badge.svg)](https://codecov.io/gh/IBM/python-sdk-core) +[![Build Status](https://app.travis-ci.com/IBM/python-sdk-core.svg?branch=main)](https://app.travis-ci.com/IBM/python-sdk-core) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ibm-cloud-sdk-core)](https://pypi.org/project/ibm-cloud-sdk-core/) [![Latest Stable Version](https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg)](https://pypi.python.org/pypi/ibm-cloud-sdk-core) [![CLA assistant](https://cla-assistant.io/readme/badge/ibm/python-sdk-core)](https://cla-assistant.io/ibm/python-sdk-core) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)[![codecov](https://codecov.io/gh/IBM/python-sdk-core/branch/main/graph/badge.svg)](https://codecov.io/gh/IBM/python-sdk-core) -# python-sdk-core -This project contains the core functionality used by Python SDK's generated by the IBM OpenAPI 3 SDK Generator (openapi-sdkgen). -Python code generated by openapi-sdkgen will depend on the function contained in this project. +# IBM Python SDK Core Version 3.12.0 +This project contains core functionality required by Python code generated by the IBM Cloud OpenAPI SDK Generator +(openapi-sdkgen). -# Notice -Support for Python versions 2.x and versions <= 3.4 is deprecated and will be officially dropped in the next major release, which is expected to be end of December, 2019. +# Python Version +The current minimum Python version supported is 3.6. ## Installation @@ -30,6 +31,7 @@ - Bearer Token - Identity and Access Management (IAM) - Cloud Pak for Data +- Container - No Authentication For more information about the various authentication types and how to use them with your services, click [here](Authentication.md) @@ -79,4 +81,4 @@ ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md). diff -Nru python-ibm-cloud-sdk-core-1.6.2/requirements-dev.txt python-ibm-cloud-sdk-core-3.12.0/requirements-dev.txt --- python-ibm-cloud-sdk-core-1.6.2/requirements-dev.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/requirements-dev.txt 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,7 @@ +codecov>=2.1.0,<3.0.0 +coverage>=4.5.4 +pylint>=2.6.0,<3.0.0 +pytest>=6.2.1,<7.0.0 +pytest-cov>=2.2.1,<3.0.0 +responses>=0.12.1,<1.0.0 +tox>=3.2.0,<4.0.0 diff -Nru python-ibm-cloud-sdk-core-1.6.2/requirements.txt python-ibm-cloud-sdk-core-3.12.0/requirements.txt --- python-ibm-cloud-sdk-core-1.6.2/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/requirements.txt 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,3 @@ +requests>=2.20,<3.0 +python_dateutil>=2.5.3,<3.0.0 +PyJWT>=2.0.1,<3.0.0 \ No newline at end of file diff -Nru python-ibm-cloud-sdk-core-1.6.2/setup.py python-ibm-cloud-sdk-core-3.12.0/setup.py --- python-ibm-cloud-sdk-core-1.6.2/setup.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/setup.py 2021-10-15 20:30:50.000000000 +0000 @@ -15,10 +15,11 @@ import os import sys +import pkg_resources from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -__version__ = '1.6.2' +__version__ = '3.12.0' if sys.argv[-1] == 'publish': # test server @@ -30,19 +31,10 @@ os.system('python setup.py sdist upload -r pypi') sys.exit() -# Convert README.md to README.rst for pypi -try: - from pypandoc import convert_file - - def read_md(f): - return convert_file(f, 'rst') -except: - print('warning: pypandoc module not found, ' - 'could not convert Markdown to RST') - - def read_md(f): - return open(f, 'rb').read().decode(encoding='utf-8') - +with open('requirements.txt') as f: + install_requires = [str(req) for req in pkg_resources.parse_requirements(f)] +with open('requirements-dev.txt') as f: + tests_require = [str(req) for req in pkg_resources.parse_requirements(f)] class PyTest(TestCommand): def finalize_options(self): @@ -55,32 +47,37 @@ errcode = pytest.main(self.test_args) sys.exit(errcode) +with open("README.md", "r") as fh: + readme = fh.read() setup(name='ibm-cloud-sdk-core', version=__version__, - description='Client library for the IBM Cloud services', + description='Core library used by SDKs for IBM Cloud Services', license='Apache 2.0', - install_requires=['requests>=2.0, <3.0', 'python_dateutil>=2.5.3', 'PyJWT >=1.7.1'], - tests_require=['responses', 'pytest', 'pytest-rerunfailures', 'tox', 'pylint', 'bumpversion'], + install_requires=install_requires, + tests_require=tests_require, cmdclass={'test': PyTest}, - author='Erika Dsouza', - author_email='erika.dsouza@ibm.com', - long_description=read_md('README.md'), + author='IBM', + author_email='devxsdk@us.ibm.com', + long_description=readme, + long_description_content_type='text/markdown', url='https://github.com/IBM/python-sdk-core', packages=find_packages(), include_package_data=True, - keywords='watson, ibm, cloud', + keywords='watson, ibm, cloud, ibm cloud services', classifiers=[ 'Programming Language :: Python', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', - 'Development Status :: 4 - Beta', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Software Development :: Libraries :: Application ' - 'Frameworks', + 'Topic :: Software Development :: Libraries :: Application Frameworks', ], zip_safe=True ) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_api_exception.py python-ibm-cloud-sdk-core-3.12.0/test/test_api_exception.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_api_exception.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_api_exception.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,9 +1,12 @@ # coding=utf-8 import json -import responses + import requests +import responses + from ibm_cloud_sdk_core import ApiException + @responses.activate def test_api_exception(): """Test APIException class""" @@ -48,7 +51,7 @@ content_type='application/json') mock_response = requests.get('https://test-msg.com') exception = ApiException(500, http_response=mock_response) - assert exception.message == 'Unknown error' + assert exception.message == 'Internal Server Error' responses.add(responses.GET, 'https://test-errormessage.com', diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_authenticator.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,21 @@ +# coding=utf-8 +# pylint: disable=missing-docstring + +from requests import Request +from ibm_cloud_sdk_core.authenticators import Authenticator + + +class TestAuthenticator(Authenticator): + """A test of the Authenticator base class""" + + def validate(self) -> None: + """Simulated validate() method.""" + + def authenticate(self, req: Request) -> None: + """Simulated authenticate() method.""" + + +def test_authenticator(): + authenticator = TestAuthenticator() + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_UNKNOWN diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_base_service.py python-ibm-cloud-sdk-core-3.12.0/test/test_base_service.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_base_service.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_base_service.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,28 +1,37 @@ # coding=utf-8 # pylint: disable=missing-docstring,protected-access,too-few-public-methods +import gzip import json -import time import os +import tempfile +import time from shutil import copyfile +from typing import Optional +from urllib3.exceptions import ConnectTimeoutError, MaxRetryError + +import jwt import pytest import responses -import jwt -from ibm_cloud_sdk_core import BaseService +import requests + from ibm_cloud_sdk_core import ApiException +from ibm_cloud_sdk_core import BaseService, DetailedResponse from ibm_cloud_sdk_core import CP4DTokenManager +from ibm_cloud_sdk_core import get_authenticator_from_environment from ibm_cloud_sdk_core.authenticators import (IAMAuthenticator, NoAuthAuthenticator, Authenticator, BasicAuthenticator, CloudPakForDataAuthenticator) -from ibm_cloud_sdk_core import get_authenticator_from_environment +from ibm_cloud_sdk_core.utils import strip_extra_slashes class IncludeExternalConfigService(BaseService): default_service_url = 'https://servicesthatincludeexternalconfig.com/api' + def __init__( - self, - api_version, - authenticator=None, - trace_id=None - ): + self, + api_version: str, + authenticator: Optional[Authenticator] = None, + trace_id: Optional[str] = None + ) -> None: BaseService.__init__( self, service_url=self.default_service_url, @@ -33,16 +42,17 @@ self.trace_id = trace_id self.configure_service('include-external-config') + class AnyServiceV1(BaseService): default_url = 'https://gateway.watsonplatform.net/test/api' def __init__( - self, - version, - service_url=default_url, - authenticator=None, - disable_ssl_verification=False - ): + self, + version: str, + service_url: str = default_url, + authenticator: Optional[Authenticator] = None, + disable_ssl_verification: bool = False + ) -> None: BaseService.__init__( self, service_url=service_url, @@ -50,7 +60,7 @@ disable_ssl_verification=disable_ssl_verification) self.version = version - def op_with_path_params(self, path0, path1): + def op_with_path_params(self, path0: str, path1: str) -> DetailedResponse: if path0 is None: raise ValueError('path0 must be provided') if path1 is None: @@ -62,24 +72,31 @@ response = self.send(request) return response - def with_http_config(self, http_config): + def with_http_config(self, http_config: dict) -> DetailedResponse: self.set_http_config(http_config) request = self.prepare_request(method='GET', url='') response = self.send(request) return response - def any_service_call(self): + def any_service_call(self) -> DetailedResponse: request = self.prepare_request(method='GET', url='') response = self.send(request) return response - def head_request(self): + def head_request(self) -> DetailedResponse: request = self.prepare_request(method='HEAD', url='') response = self.send(request) return response + def get_document_as_stream(self) -> DetailedResponse: + params = {'version': self.version} + url = '/v1/streamjson' + request = self.prepare_request(method='GET', url=url, params=params) + response = self.send(request, stream=True) + return response + -def get_access_token(): +def get_access_token() -> str: access_token_layout = { "username": "dummy", "role": "Admin", @@ -99,7 +116,14 @@ headers={ 'kid': '230498151c214b788dd97f22b85410a5' }) - return access_token.decode('utf-8') + return access_token + + +def test_invalid_authenticator(): + with pytest.raises(ValueError) as err: + AnyServiceV1('2021-08-18') + + assert str(err.value) == 'authenticator must be provided' @responses.activate @@ -110,7 +134,7 @@ path0 = ' \"<>^`{}|/\\?#%[]' path0_encoded = '%20%22%3C%3E%5E%60%7B%7D%7C%2F%5C%3F%23%25%5B%5D' # All non-ASCII chars _must_ be encoded in path segments - path1 = u'比萨浇头'.encode('utf8') # "pizza toppings" + path1 = '比萨浇头'.encode('utf8') # "pizza toppings" path1_encoded = '%E6%AF%94%E8%90%A8%E6%B5%87%E5%A4%B4' path_encoded = '/v1/foo/' + path0_encoded + '/bar/' + path1_encoded + '/baz' @@ -125,6 +149,9 @@ }), content_type='application/json') + # Set Host as a default header on the service. + service.set_default_headers({'Host': 'alternatehost.ibm.com:443'}) + response = service.op_with_path_params(path0, path1) assert response is not None @@ -132,6 +159,50 @@ assert path_encoded in responses.calls[0].request.url assert 'version=2017-07-07' in responses.calls[0].request.url + # Verify that the Host header was set in the request. + assert responses.calls[0].request.headers.get( + 'Host') == 'alternatehost.ibm.com:443' + + +@responses.activate +def test_stream_json_response(): + service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) + + path = '/v1/streamjson' + test_url = service.default_url + path + + expected_response = json.dumps( + {"id": 1, "rev": "v1", "content": "this is a document"}) + + # print("Expected response: ", expected_response) + + # Simulate a JSON response + responses.add( + responses.GET, + test_url, + status=200, + body=expected_response, + content_type='application/json') + + # Invoke the operation and receive an "iterable" as the response + response = service.get_document_as_stream() + + assert response is not None + assert len(responses.calls) == 1 + + # retrieve the requests.Response object from the DetailedResponse + resp = response.get_result() + assert isinstance(resp, requests.Response) + assert hasattr(resp, "iter_content") + + # Retrieve the response body, one chunk at a time. + actual_response = '' + for chunk in resp.iter_content(chunk_size=3): + actual_response += chunk.decode("utf-8") + + # print("Actual response: ", actual_response) + assert actual_response == expected_response + @responses.activate def test_http_config(): @@ -155,6 +226,7 @@ with pytest.raises(TypeError): service.with_http_config(None) + @responses.activate def test_cwd(): file_path = os.path.join( @@ -179,6 +251,7 @@ assert service.service_url == 'https://cwdserviceurl' assert service.authenticator is not None + @responses.activate def test_iam(): file_path = os.path.join( @@ -212,6 +285,7 @@ service.any_service_call() assert "grant-type%3Aapikey" in responses.calls[0].request.body + def test_no_auth(): class MadeUp: def __init__(self): @@ -241,12 +315,13 @@ assert service.authenticator.token_manager is not None assert service.authenticator.token_manager.username == 'my_username' assert service.authenticator.token_manager.password == 'my_password' - assert service.authenticator.token_manager.url == 'my_url/v1/preauth/validateAuth' + assert service.authenticator.token_manager.url == 'my_url/v1/authorize' assert isinstance(service.authenticator.token_manager, CP4DTokenManager) def test_disable_ssl_verification(): - service1 = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator(), disable_ssl_verification=True) + service1 = AnyServiceV1( + '2017-07-07', authenticator=NoAuthAuthenticator(), disable_ssl_verification=True) assert service1.disable_ssl_verification is True service1.set_disable_ssl_verification(False) @@ -291,7 +366,8 @@ def test_has_bad_first_or_last_char(): with pytest.raises(ValueError) as err: - basic_authenticator = BasicAuthenticator('{my_username}', 'my_password') + basic_authenticator = BasicAuthenticator( + '{my_username}', 'my_password') AnyServiceV1('2018-11-20', authenticator=basic_authenticator).prepare_request( responses.GET, 'https://gateway.watsonplatform.net/test/api' @@ -301,6 +377,7 @@ ) == 'The username and password shouldn\'t start or end with curly brackets or quotes. '\ 'Please remove any surrounding {, }, or \" characters.' + @responses.activate def test_request_server_error(): responses.add( @@ -318,6 +395,7 @@ except ApiException as err: assert err.message == 'internal server error' + @responses.activate def test_request_success_json(): responses.add( @@ -333,13 +411,15 @@ detailed_response = service.send(prepped) assert detailed_response.get_result() == {'foo': 'bar'} - service = AnyServiceV1('2018-11-20', authenticator=BasicAuthenticator('my_username', 'my_password')) + service = AnyServiceV1( + '2018-11-20', authenticator=BasicAuthenticator('my_username', 'my_password')) service.set_default_headers({'test': 'header'}) service.set_disable_ssl_verification(True) prepped = service.prepare_request('GET', url='') detailed_response = service.send(prepped) assert detailed_response.get_result() == {'foo': 'bar'} + @responses.activate def test_request_success_response(): responses.add( @@ -355,6 +435,7 @@ detailed_response = service.send(prepped) assert detailed_response.get_result() == {"foo": "bar"} + @responses.activate def test_request_fail_401(): responses.add( @@ -372,6 +453,7 @@ except ApiException as err: assert err.message == 'Unauthorized: Access is denied due to invalid credentials' + def test_misc_methods(): class MockModel: @@ -405,6 +487,11 @@ res_str = service._convert_list(temp) assert res_str == 'default,123' + temp2 = 'default123' + res_str2 = service._convert_list(temp2) + assert res_str2 == temp2 + + def test_default_headers(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) service.set_default_headers({'xxx': 'yyy'}) @@ -412,6 +499,7 @@ with pytest.raises(TypeError): service.set_default_headers('xxx') + def test_set_service_url(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) with pytest.raises(ValueError) as err: @@ -421,11 +509,171 @@ service.set_service_url('my_url') + +def test_http_client(): + auth = BasicAuthenticator('my_username', 'my_password') + service = AnyServiceV1('2018-11-20', authenticator=auth) + assert isinstance(service.get_http_client(), requests.sessions.Session) + assert service.get_http_client().headers.get( + 'Accept-Encoding') == 'gzip, deflate' + + new_http_client = requests.Session() + new_http_client.headers.update({'Accept-Encoding': 'gzip'}) + service.set_http_client(http_client=new_http_client) + assert service.get_http_client().headers.get('Accept-Encoding') == 'gzip' + + with pytest.raises(TypeError): + service.set_http_client("bad_argument_type") + + def test_get_authenticator(): auth = BasicAuthenticator('my_username', 'my_password') service = AnyServiceV1('2018-11-20', authenticator=auth) assert service.get_authenticator() is not None + +def test_gzip_compression(): + # Should return uncompressed data when gzip is off + service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) + assert not service.get_enable_gzip_compression() + prepped = service.prepare_request( + 'GET', url='', data=json.dumps({"foo": "bar"})) + assert prepped['data'] == b'{"foo": "bar"}' + assert prepped['headers'].get('content-encoding') != 'gzip' + + # Should return compressed data when gzip is on + service.set_enable_gzip_compression(True) + assert service.get_enable_gzip_compression() + prepped = service.prepare_request( + 'GET', url='', data=json.dumps({"foo": "bar"})) + assert prepped['data'] == gzip.compress(b'{"foo": "bar"}') + assert prepped['headers'].get('content-encoding') == 'gzip' + + # Should return compressed data when gzip is on for non-json data + assert service.get_enable_gzip_compression() + prepped = service.prepare_request('GET', url='', data=b'rawdata') + assert prepped['data'] == gzip.compress(b'rawdata') + assert prepped['headers'].get('content-encoding') == 'gzip' + + # Should return compressed data when gzip is on for gzip file data + assert service.get_enable_gzip_compression() + with tempfile.TemporaryFile(mode='w+b') as t_f: + with gzip.GzipFile(mode='wb', fileobj=t_f) as gz_f: + gz_f.write(json.dumps({"foo": "bar"}).encode()) + with gzip.GzipFile(mode='rb', fileobj=t_f) as gz_f: + gzip_data = gz_f.read() + prepped = service.prepare_request('GET', url='', data=gzip_data) + assert prepped['data'] == gzip.compress(t_f.read()) + assert prepped['headers'].get('content-encoding') == 'gzip' + + # Should return compressed json data when gzip is on for gzip file json data + assert service.get_enable_gzip_compression() + with tempfile.TemporaryFile(mode='w+b') as t_f: + with gzip.GzipFile(mode='wb', fileobj=t_f) as gz_f: + gz_f.write("rawdata".encode()) + with gzip.GzipFile(mode='rb', fileobj=t_f) as gz_f: + gzip_data = gz_f.read() + prepped = service.prepare_request('GET', url='', data=gzip_data) + assert prepped['data'] == gzip.compress(t_f.read()) + assert prepped['headers'].get('content-encoding') == 'gzip' + + # Should return uncompressed data when content-encoding is set + assert service.get_enable_gzip_compression() + prepped = service.prepare_request('GET', url='', headers={"content-encoding": "gzip"}, + data=json.dumps({"foo": "bar"})) + assert prepped['data'] == b'{"foo": "bar"}' + assert prepped['headers'].get('content-encoding') == 'gzip' + + +def test_gzip_compression_external(): + # Should set gzip compression from external config + file_path = os.path.join( + os.path.dirname(__file__), '../resources/ibm-credentials-gzip.env') + os.environ['IBM_CREDENTIALS_FILE'] = file_path + service = IncludeExternalConfigService( + 'v1', authenticator=NoAuthAuthenticator()) + assert service.service_url == 'https://mockurl' + assert service.get_enable_gzip_compression() is True + prepped = service.prepare_request( + 'GET', url='', data=json.dumps({"foo": "bar"})) + assert prepped['data'] == gzip.compress(b'{"foo": "bar"}') + assert prepped['headers'].get('content-encoding') == 'gzip' + + +def test_retry_config_default(): + service = BaseService(service_url='https://mockurl/', + authenticator=NoAuthAuthenticator()) + service.enable_retries() + assert service.retry_config.total == 4 + assert service.retry_config.backoff_factor == 1.0 + assert service.http_client.get_adapter('https://').max_retries.total == 4 + + # Ensure retries fail after 4 retries + error = ConnectTimeoutError() + retry = service.http_client.get_adapter('https://').max_retries + retry = retry.increment(error=error) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + with pytest.raises(MaxRetryError) as retry_err: + retry.increment(error=error) + assert retry_err.value.reason == error + + +def test_retry_config_disable(): + # Test disabling retries + service = BaseService(service_url='https://mockurl/', + authenticator=NoAuthAuthenticator()) + service.enable_retries() + service.disable_retries() + assert service.retry_config is None + assert service.http_client.get_adapter('https://').max_retries.total == 0 + + # Ensure retries are not started after one connection attempt + error = ConnectTimeoutError() + retry = service.http_client.get_adapter('https://').max_retries + with pytest.raises(MaxRetryError) as retry_err: + retry.increment(error=error) + assert retry_err.value.reason == error + + +def test_retry_config_non_default(): + service = BaseService(service_url='https://mockurl/', + authenticator=NoAuthAuthenticator()) + service.enable_retries(2, 0.3) + assert service.retry_config.total == 2 + assert service.retry_config.backoff_factor == 0.3 + + # Ensure retries fail after 2 retries + error = ConnectTimeoutError() + retry = service.http_client.get_adapter('https://').max_retries + retry = retry.increment(error=error) + retry = retry.increment(error=error) + with pytest.raises(MaxRetryError) as retry_err: + retry.increment(error=error) + assert retry_err.value.reason == error + + +def test_retry_config_external(): + file_path = os.path.join( + os.path.dirname(__file__), '../resources/ibm-credentials-retry.env') + os.environ['IBM_CREDENTIALS_FILE'] = file_path + service = IncludeExternalConfigService( + 'v1', authenticator=NoAuthAuthenticator()) + assert service.retry_config.total == 3 + assert service.retry_config.backoff_factor == 0.2 + + # Ensure retries fail after 3 retries + error = ConnectTimeoutError() + retry = service.http_client.get_adapter('https://').max_retries + retry = retry.increment(error=error) + retry = retry.increment(error=error) + retry = retry.increment(error=error) + with pytest.raises(MaxRetryError) as retry_err: + retry.increment(error=error) + assert retry_err.value.reason == error + + @responses.activate def test_user_agent_header(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) @@ -450,16 +698,54 @@ assert response.get_result().request.headers.__getitem__( 'user-agent') == user_agent_header['User-Agent'] +@responses.activate +def test_reserved_keys(caplog): + service = AnyServiceV1('2021-07-02', authenticator=NoAuthAuthenticator()) + responses.add( + responses.GET, + 'https://gateway.watsonplatform.net/test/api', + status=200, + body='some text') + prepped = service.prepare_request('GET', url='', headers={'key': 'OK'}) + response = service.send( + prepped, + headers={'key': 'bad'}, + method='POST', + url='localhost', + cookies=None, + hooks={'response': []}) + assert response.get_result().request.headers.__getitem__('key') == 'OK' + assert response.get_result().request.url == 'https://gateway.watsonplatform.net/test/api' + assert response.get_result().request.method == 'GET' + assert response.get_result().request.hooks == {'response': []} + assert caplog.record_tuples[0][2] == '"method" has been removed from the request' + assert caplog.record_tuples[1][2] == '"url" has been removed from the request' + assert caplog.record_tuples[2][2] == '"headers" has been removed from the request' + assert caplog.record_tuples[3][2] == '"cookies" has been removed from the request' + +@responses.activate +def test_ssl_error(): + responses.add( + responses.GET, + 'https://gateway.watsonplatform.net/test/api', + body=requests.exceptions.SSLError()) + service = AnyServiceV1('2021-08-18', authenticator=NoAuthAuthenticator()) + with pytest.raises(requests.exceptions.SSLError): + prepped = service.prepare_request('GET', url='') + service.send(prepped) + + def test_files_dict(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = {} - file = open( + with open( os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') - form_data['file1'] = (None, file, 'application/octet-stream') + os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8') as file: + form_data['file1'] = (None, file, 'application/octet-stream') form_data['string1'] = (None, 'hello', 'text/plain') - request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) + request = service.prepare_request( + 'GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 2 @@ -469,16 +755,18 @@ string1 = files_dict['string1'] assert string1[0] is None + def test_files_list(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = [] - file = open( + with open( os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') - form_data.append(('file1', (None, file, 'application/octet-stream'))) + os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8') as file: + form_data.append(('file1', (None, file, 'application/octet-stream'))) form_data.append(('string1', (None, 'hello', 'text/plain'))) - request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) + request = service.prepare_request( + 'GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 2 @@ -488,23 +776,28 @@ string1 = files_dict['string1'] assert string1[0] is None + def test_files_duplicate_parts(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = [] - file = open( + with open( os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r') - form_data.append(('creds_file', (None, file, 'application/octet-stream'))) - file = open( + os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8') as file: + form_data.append( + ('creds_file', (None, file, 'application/octet-stream'))) + with open( os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-basic.env'), 'r') - form_data.append(('creds_file', (None, file, 'application/octet-stream'))) - file = open( + os.path.dirname(__file__), '../resources/ibm-credentials-basic.env'), 'r', encoding='utf-8') as file: + form_data.append( + ('creds_file', (None, file, 'application/octet-stream'))) + with open( os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env'), 'r') - form_data.append(('creds_file', (None, file, 'application/octet-stream'))) - request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) + os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env'), 'r', encoding='utf-8') as file: + form_data.append( + ('creds_file', (None, file, 'application/octet-stream'))) + request = service.prepare_request( + 'GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 3 @@ -512,10 +805,62 @@ assert part_name == 'creds_file' assert file_tuple[0] is not None + def test_json(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) - req = service.prepare_request('POST', url='', headers={'X-opt-out': True}, data={'hello': 'world'}) - assert req.get('data') == "{\"hello\": \"world\"}" + req = service.prepare_request('POST', url='', headers={ + 'X-opt-out': True}, data={'hello': 'world', 'fóó': 'bår'}) + assert req.get('data') == b'{"hello": "world", "f\\u00f3\\u00f3": "b\\u00e5r"}' + + +def test_trailing_slash(): + assert strip_extra_slashes('') == '' + assert strip_extra_slashes('//') == '/' + assert strip_extra_slashes('/////') == '/' + assert strip_extra_slashes('https://host') == 'https://host' + assert strip_extra_slashes('https://host/') == 'https://host/' + assert strip_extra_slashes('https://host//') == 'https://host/' + assert strip_extra_slashes('https://host/path') == 'https://host/path' + assert strip_extra_slashes('https://host/path/') == 'https://host/path/' + assert strip_extra_slashes('https://host/path//') == 'https://host/path/' + assert strip_extra_slashes('https://host//path//') == 'https://host//path/' + + service = AnyServiceV1( + '2018-11-20', service_url='https://host/', authenticator=NoAuthAuthenticator()) + assert service.service_url == 'https://host/' + service.set_service_url('https://host/') + assert service.service_url == 'https://host/' + req = service.prepare_request('POST', + url='/path/', + headers={'X-opt-out': True}, + data={'hello': 'world'}) + assert req.get('url') == 'https://host//path/' + + service = AnyServiceV1( + '2018-11-20', service_url='https://host/', authenticator=NoAuthAuthenticator()) + assert service.service_url == 'https://host/' + service.set_service_url('https://host/') + assert service.service_url == 'https://host/' + req = service.prepare_request('POST', + url='/', + headers={'X-opt-out': True}, + data={'hello': 'world'}) + assert req.get('url') == 'https://host/' + + service.set_service_url(None) + assert service.service_url is None + + service = AnyServiceV1('2018-11-20', service_url='/', + authenticator=NoAuthAuthenticator()) + assert service.service_url == '/' + service.set_service_url('/') + assert service.service_url == '/' + req = service.prepare_request('POST', + url='/', + headers={'X-opt-out': True}, + data={'hello': 'world'}) + assert req.get('url') == '/' + def test_service_url_not_set(): service = BaseService(service_url='', authenticator=NoAuthAuthenticator()) @@ -523,8 +868,10 @@ service.prepare_request('POST', url='') assert str(err.value) == 'The service_url is required' + def test_setting_proxy(): - service = BaseService('test', authenticator=IAMAuthenticator('wonder woman')) + service = BaseService(service_url='test', + authenticator=IAMAuthenticator('wonder woman')) assert service.authenticator is not None assert service.authenticator.token_manager.http_config == {} @@ -536,22 +883,27 @@ service.set_http_config(http_config) assert service.authenticator.token_manager.http_config == http_config - service2 = BaseService('test', authenticator=BasicAuthenticator('marvellous', 'mrs maisel')) + service2 = BaseService(service_url='test', authenticator=BasicAuthenticator( + 'marvellous', 'mrs maisel')) service2.set_http_config(http_config) assert service2.authenticator is not None + def test_configure_service(): file_path = os.path.join( os.path.dirname(__file__), '../resources/ibm-credentials-external.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path - service = IncludeExternalConfigService('v1', authenticator=NoAuthAuthenticator()) + service = IncludeExternalConfigService( + 'v1', authenticator=NoAuthAuthenticator()) assert service.service_url == 'https://externallyconfigured.com/api' assert service.disable_ssl_verification is True # The authenticator should not be changed as a result of configure_service() assert isinstance(service.get_authenticator(), NoAuthAuthenticator) + def test_configure_service_error(): - service = BaseService('v1', authenticator=NoAuthAuthenticator()) + service = BaseService( + service_url='v1', authenticator=NoAuthAuthenticator()) with pytest.raises(ValueError) as err: service.configure_service(None) assert str(err.value) == 'Service_name must be of type string.' diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_basic_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_basic_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_basic_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_basic_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,10 +1,13 @@ # pylint: disable=missing-docstring import pytest -from ibm_cloud_sdk_core.authenticators import BasicAuthenticator + +from ibm_cloud_sdk_core.authenticators import BasicAuthenticator, Authenticator + def test_basic_authenticator(): authenticator = BasicAuthenticator('my_username', 'my_password') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'my_username' assert authenticator.password == 'my_password' @@ -12,6 +15,7 @@ authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Basic bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=' + def test_basic_authenticator_validate_failed(): with pytest.raises(ValueError) as err: BasicAuthenticator('my_username', None) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_bearer_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_bearer_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_bearer_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_bearer_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,11 +1,13 @@ # pylint: disable=missing-docstring import pytest -from ibm_cloud_sdk_core.authenticators import BearerTokenAuthenticator +from ibm_cloud_sdk_core.authenticators import BearerTokenAuthenticator, Authenticator + def test_bearer_authenticator(): authenticator = BearerTokenAuthenticator('my_bearer_token') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BEARERTOKEN assert authenticator.bearer_token == 'my_bearer_token' authenticator.set_bearer_token('james bond') @@ -15,6 +17,7 @@ authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer james bond' + def test_bearer_validate_failed(): with pytest.raises(ValueError) as err: BearerTokenAuthenticator(None) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_container_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_container_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_container_authenticator.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_container_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,105 @@ +# pylint: disable=missing-docstring +import pytest + +from ibm_cloud_sdk_core.authenticators import ContainerAuthenticator, Authenticator + + +def test_container_authenticator(): + authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER + assert authenticator.token_manager.cr_token_filename is None + assert authenticator.token_manager.iam_profile_name == 'iam-user-123' + assert authenticator.token_manager.iam_profile_id is None + assert authenticator.token_manager.client_id is None + assert authenticator.token_manager.client_secret is None + assert authenticator.token_manager.disable_ssl_verification is False + assert authenticator.token_manager.headers is None + assert authenticator.token_manager.proxies is None + assert authenticator.token_manager.scope is None + + authenticator.set_cr_token_filename('path/to/token') + assert authenticator.token_manager.cr_token_filename == 'path/to/token' + + # Set the IAM profile to None to trigger a validation which will fail, + # because both of the profile and ID are None. + with pytest.raises(ValueError) as err: + authenticator.set_iam_profile_name(None) + assert str( + err.value) == 'At least one of iam_profile_name or iam_profile_id must be specified.' + + authenticator.set_iam_profile_id('iam-id-123') + assert authenticator.token_manager.iam_profile_id == 'iam-id-123' + + authenticator.set_iam_profile_name('iam-user-123') + assert authenticator.token_manager.iam_profile_name == 'iam-user-123' + + authenticator.set_client_id_and_secret('tom', 'jerry') + assert authenticator.token_manager.client_id == 'tom' + assert authenticator.token_manager.client_secret == 'jerry' + + authenticator.set_scope('scope1 scope2 scope3') + assert authenticator.token_manager.scope == 'scope1 scope2 scope3' + + with pytest.raises(TypeError) as err: + authenticator.set_headers('dummy') + assert str(err.value) == 'headers must be a dictionary' + + authenticator.set_headers({'dummy': 'headers'}) + assert authenticator.token_manager.headers == {'dummy': 'headers'} + + with pytest.raises(TypeError) as err: + authenticator.set_proxies('dummy') + assert str(err.value) == 'proxies must be a dictionary' + + authenticator.set_proxies({'dummy': 'proxies'}) + assert authenticator.token_manager.proxies == {'dummy': 'proxies'} + + +def test_disable_ssl_verification(): + authenticator = ContainerAuthenticator( + iam_profile_name='iam-user-123', disable_ssl_verification=True) + assert authenticator.token_manager.disable_ssl_verification is True + + authenticator.set_disable_ssl_verification(False) + assert authenticator.token_manager.disable_ssl_verification is False + + +def test_invalid_disable_ssl_verification_type(): + with pytest.raises(TypeError) as err: + authenticator = ContainerAuthenticator( + iam_profile_name='iam-user-123', disable_ssl_verification='True') + assert str(err.value) == 'disable_ssl_verification must be a bool' + + authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123') + assert authenticator.token_manager.disable_ssl_verification is False + + with pytest.raises(TypeError) as err: + authenticator.set_disable_ssl_verification('True') + assert str(err.value) == 'status must be a bool' + + +def test_container_authenticator_with_scope(): + authenticator = ContainerAuthenticator( + iam_profile_name='iam-user-123', scope='scope1 scope2') + assert authenticator is not None + assert authenticator.token_manager.scope == 'scope1 scope2' + + +def test_authenticator_validate_failed(): + with pytest.raises(ValueError) as err: + ContainerAuthenticator(None) + assert str( + err.value) == 'At least one of iam_profile_name or iam_profile_id must be specified.' + + with pytest.raises(ValueError) as err: + ContainerAuthenticator( + iam_profile_name='iam-user-123', client_id='my_client_id') + assert str( + err.value) == 'Both client_id and client_secret should be initialized.' + + with pytest.raises(ValueError) as err: + ContainerAuthenticator( + iam_profile_name='iam-user-123', client_secret='my_client_secret') + assert str( + err.value) == 'Both client_id and client_secret should be initialized.' diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_container_token_manager.py python-ibm-cloud-sdk-core-3.12.0/test/test_container_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_container_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_container_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,283 @@ +# pylint: disable=missing-docstring +import json +import os +import time +from urllib.parse import parse_qs + +import responses +import pytest + +from ibm_cloud_sdk_core import ApiException, ContainerTokenManager +from ibm_cloud_sdk_core.authenticators import ContainerAuthenticator + +# pylint: disable=line-too-long +TEST_ACCESS_TOKEN_1 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' +TEST_ACCESS_TOKEN_2 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJ1c2VybmFtZSI6ImR1bW15Iiwicm9sZSI6IkFkbWluIiwicGVybWlzc2lvbnMiOlsiYWRtaW5pc3RyYXRvciIsIm1hbmFnZV9jYXRhbG9nIl0sInN1YiI6ImFkbWluIiwiaXNzIjoic3NzIiwiYXVkIjoic3NzIiwidWlkIjoic3NzIiwiaWF0IjozNjAwLCJleHAiOjE2MjgwMDcwODF9.zvUDpgqWIWs7S1CuKv40ERw1IZ5FqSFqQXsrwZJyfRM' +TEST_REFRESH_TOKEN = 'Xj7Gle500MachEOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' +MOCK_IAM_PROFILE_NAME = 'iam-user-123' +MOCK_CLIENT_ID = 'client-id-1' +MOCK_CLIENT_SECRET = 'client-secret-1' + +cr_token_file = os.path.join(os.path.dirname(__file__), '../resources/cr-token.txt') + + +def _get_current_time() -> int: + return int(time.time()) + + +def mock_iam_response(func): + """This is decorator function which extends `responses.activate`. + This sets up all the mock response stuffs. + """ + def callback(request): + assert request.headers['Accept'] == 'application/json' + assert request.headers['Content-Type'] == 'application/x-www-form-urlencoded' + + payload = parse_qs(request.body) + + assert payload['cr_token'][0] == 'cr-token-1' + assert payload['grant_type'][0] == 'urn:ibm:params:oauth:grant-type:cr-token' + assert payload.get('profile_name', [None])[0] or payload.get('profile_id', [None])[0] + + status_code = 200 + + scope = payload.get('scope')[0] if payload.get('scope') else None + if scope == 'send-second-token': + access_token = TEST_ACCESS_TOKEN_2 + elif scope == 'status-bad-request': + access_token = None + status_code = 400 + elif scope == 'check-basic-auth': + assert request.headers['Authorization'] == 'Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x' + access_token = TEST_ACCESS_TOKEN_1 + else: + access_token = TEST_ACCESS_TOKEN_1 + + response = json.dumps({ + 'access_token': access_token, + 'token_type': 'Bearer', + 'expires_in': 3600, + 'expiration': _get_current_time()+3600, + 'refresh_token': TEST_REFRESH_TOKEN, + }) + + return (status_code, {}, response) + + @responses.activate + def wrapper(): + response = responses.CallbackResponse( + method=responses.POST, + url='https://iam.cloud.ibm.com/identity/token', + callback=callback, + ) + + responses.add(response) + + func() + + return wrapper + + +@mock_iam_response +def test_request_token_auth_default(): + iam_url = "https://iam.cloud.ibm.com/identity/token" + + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + ) + token_manager.request_token() + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == iam_url + assert responses.calls[0].request.headers.get('Authorization') is None + assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 + + +@mock_iam_response +def test_request_token_auth_in_ctor(): + default_auth_header = 'Basic Yng6Yng=' + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + client_id='foo', + client_secret='bar') + + token_manager.request_token() + + assert len(responses.calls) == 1 + assert responses.calls[0].request.headers['Authorization'] != default_auth_header + assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 + assert 'scope' not in responses.calls[0].response.request.body + + +@mock_iam_response +def test_request_token_auth_in_ctor_with_scope(): + default_auth_header = 'Basic Yng6Yng=' + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + client_id='foo', + client_secret='bar', + scope='john snow') + + token_manager.request_token() + + assert len(responses.calls) == 1 + assert responses.calls[0].request.headers['Authorization'] != default_auth_header + assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 + assert 'scope=john+snow' in responses.calls[0].response.request.body + + +def test_retrieve_cr_token_success(): + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + ) + + cr_token = token_manager.retrieve_cr_token() + + assert cr_token == 'cr-token-1' + + +def test_retrieve_cr_token_fail(): + token_manager = ContainerTokenManager( + cr_token_filename='bogus-cr-token-file', + ) + + with pytest.raises(Exception) as err: + token_manager.retrieve_cr_token() + + assert str(err.value) == 'Unable to retrieve the CR token value from file bogus-cr-token-file: [Errno 2] No such file or directory: \'bogus-cr-token-file\'' + + +@mock_iam_response +def test_get_token_success(): + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + ) + + access_token = token_manager.access_token + assert access_token is None + + access_token = token_manager.get_token() + assert access_token == TEST_ACCESS_TOKEN_1 + assert token_manager.access_token == TEST_ACCESS_TOKEN_1 + + # Verify the token manager return the cached value. + # Before we call the `get_token` again, set the expiration and time. + # This is necessary because we are using a fix JWT response. + token_manager.expire_time = _get_current_time() + 3600 + token_manager.refresh_time = _get_current_time() + 3600 + token_manager.set_scope('send-second-token') + access_token = token_manager.get_token() + assert access_token == TEST_ACCESS_TOKEN_1 + assert token_manager.access_token == TEST_ACCESS_TOKEN_1 + + # Force expiration to get the second token. + token_manager.expire_time = _get_current_time() - 1 + access_token = token_manager.get_token() + assert access_token == TEST_ACCESS_TOKEN_2 + assert token_manager.access_token == TEST_ACCESS_TOKEN_2 + + +@mock_iam_response +def test_request_token_success(): + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + ) + + token_response = token_manager.request_token() + assert token_response['access_token'] == TEST_ACCESS_TOKEN_1 + + +@mock_iam_response +def test_authenticate_success(): + authenticator = ContainerAuthenticator( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME) + + request = {'headers': {}} + + authenticator.authenticate(request) + assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_1 + + # Verify the token manager return the cached value. + # Before we call the `get_token` again, set the expiration and time. + # This is necessary because we are using a fix JWT response. + authenticator.token_manager.expire_time = _get_current_time() + 3600 + authenticator.token_manager.refresh_time = _get_current_time() + 3600 + authenticator.token_manager.set_scope('send-second-token') + authenticator.authenticate(request) + assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_1 + + # Force expiration to get the second token. + authenticator.token_manager.expire_time = _get_current_time() - 1 + authenticator.authenticate(request) + assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_2 + + +@mock_iam_response +def test_authenticate_fail_no_cr_token(): + authenticator = ContainerAuthenticator( + cr_token_filename='bogus-cr-token-file', + iam_profile_name=MOCK_IAM_PROFILE_NAME, + url='https://bogus.iam.endpoint') + + request = {'headers': {}} + + with pytest.raises(Exception) as err: + authenticator.authenticate(request) + + assert str(err.value) == 'Unable to retrieve the CR token value from file bogus-cr-token-file: [Errno 2] No such file or directory: \'bogus-cr-token-file\'' + + +@mock_iam_response +def test_authenticate_fail_iam(): + authenticator = ContainerAuthenticator( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + scope='status-bad-request') + + request = {'headers': {}} + + with pytest.raises(ApiException) as err: + authenticator.authenticate(request) + + assert str(err.value) == 'Error: Bad Request, Code: 400' + + +@mock_iam_response +def test_client_id_and_secret(): + token_manager = ContainerTokenManager( + cr_token_filename=cr_token_file, + iam_profile_name=MOCK_IAM_PROFILE_NAME, + ) + + token_manager.set_client_id_and_secret(MOCK_CLIENT_ID, MOCK_CLIENT_SECRET) + token_manager.set_scope('check-basic-auth') + access_token = token_manager.get_token() + assert access_token == TEST_ACCESS_TOKEN_1 + +@mock_iam_response +def test_setter_methods(): + token_manager = ContainerTokenManager( + cr_token_filename='bogus-cr-token-file', + iam_profile_name=MOCK_IAM_PROFILE_NAME, + ) + + assert token_manager.iam_profile_id is None + assert token_manager.iam_profile_name == MOCK_IAM_PROFILE_NAME + assert token_manager.cr_token_filename == 'bogus-cr-token-file' + + token_manager.set_iam_profile_id('iam-id-123') + token_manager.set_iam_profile_name(None) + token_manager.set_cr_token_filename(cr_token_file) + + assert token_manager.iam_profile_id == 'iam-id-123' + assert token_manager.iam_profile_name is None + assert token_manager.cr_token_filename == cr_token_file + + access_token = token_manager.get_token() + assert access_token == TEST_ACCESS_TOKEN_1 diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_cp4d_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_cp4d_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_cp4d_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_cp4d_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,21 +1,24 @@ # pylint: disable=missing-docstring import json +import jwt import pytest import responses -import jwt -from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator +from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator, Authenticator + -def test_iam_authenticator(): +def test_cp4d_authenticator(): authenticator = CloudPakForDataAuthenticator( 'my_username', 'my_password', 'http://my_url') assert authenticator is not None - assert authenticator.token_manager.url == 'http://my_url/v1/preauth/validateAuth' + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CP4D + assert authenticator.token_manager.url == 'http://my_url/v1/authorize' assert authenticator.token_manager.username == 'my_username' assert authenticator.token_manager.password == 'my_password' assert authenticator.token_manager.disable_ssl_verification is False - assert authenticator.token_manager.headers is None + assert authenticator.token_manager.headers == { + 'Content-Type': 'application/json'} assert authenticator.token_manager.proxies is None authenticator.set_disable_ssl_verification(True) @@ -36,20 +39,65 @@ assert authenticator.token_manager.proxies == {'dummy': 'proxies'} -def test_iam_authenticator_validate_failed(): +def test_disable_ssl_verification(): + authenticator = CloudPakForDataAuthenticator( + 'my_username', 'my_password', 'http://my_url', disable_ssl_verification=True) + assert authenticator.token_manager.disable_ssl_verification is True + + authenticator.set_disable_ssl_verification(False) + assert authenticator.token_manager.disable_ssl_verification is False + + +def test_invalid_disable_ssl_verification_type(): + with pytest.raises(TypeError) as err: + authenticator = CloudPakForDataAuthenticator( + 'my_username', 'my_password', 'http://my_url', disable_ssl_verification='True') + assert str(err.value) == 'disable_ssl_verification must be a bool' + + authenticator = CloudPakForDataAuthenticator( + 'my_username', 'my_password', 'http://my_url') + assert authenticator.token_manager.disable_ssl_verification is False + + with pytest.raises(TypeError) as err: + authenticator.set_disable_ssl_verification('True') + assert str(err.value) == 'status must be a bool' + + +def test_cp4d_authenticator_validate_failed(): with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', None, 'my_url') - assert str(err.value) == 'The username and password shouldn\'t be None.' + assert str( + err.value) == 'Exactly one of `apikey` or `password` must be specified.' + + with pytest.raises(ValueError) as err: + CloudPakForDataAuthenticator(username='my_username', url='my_url') + assert str( + err.value) == 'Exactly one of `apikey` or `password` must be specified.' + + with pytest.raises(ValueError) as err: + CloudPakForDataAuthenticator( + 'my_username', None, 'my_url', apikey=None) + assert str( + err.value) == 'Exactly one of `apikey` or `password` must be specified.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator(None, 'my_password', 'my_url') - assert str(err.value) == 'The username and password shouldn\'t be None.' + assert str(err.value) == 'The username shouldn\'t be None.' + + with pytest.raises(ValueError) as err: + CloudPakForDataAuthenticator(password='my_password', url='my_url') + assert str(err.value) == 'The username shouldn\'t be None.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', 'my_password', None) assert str(err.value) == 'The url shouldn\'t be None.' with pytest.raises(ValueError) as err: + CloudPakForDataAuthenticator( + username='my_username', password='my_password') + assert str(err.value) == 'The url shouldn\'t be None.' + + with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('{my_username}', 'my_password', 'my_url') assert str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. '\ 'Please remove any surrounding {, }, or \" characters.' @@ -85,16 +133,36 @@ access_token = jwt.encode(access_token_layout, 'secret', algorithm='HS256', - headers={'kid': '230498151c214b788dd97f22b85410a5'}).decode('utf-8') + headers={'kid': '230498151c214b788dd97f22b85410a5'}) response = { - "accessToken": access_token, + "token": access_token, "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" } - responses.add(responses.GET, url + '/v1/preauth/validateAuth', body=json.dumps(response), status=200) + responses.add(responses.POST, url + '/v1/authorize', + body=json.dumps(response), status=200) + + auth_headers = {'Host': 'cp4d.cloud.ibm.com:443'} + authenticator = CloudPakForDataAuthenticator( + 'my_username', 'my_password', url + '/v1/authorize', + headers=auth_headers) + + # Simulate an SDK API request that needs to be authenticated. + request = {'headers': {}} + + # Trigger the "get token" processing to obtain the access token and add to the "SDK request". + authenticator.authenticate(request) + + # Verify that the "authenticate()" method added the Authorization header + assert request['headers']['Authorization'] is not None + + # Verify that the "get token" call contained the Host header. + assert responses.calls[0].request.headers.get( + 'Host') == 'cp4d.cloud.ibm.com:443' + # Ensure '/v1/authorize' is added to the url if omitted authenticator = CloudPakForDataAuthenticator( 'my_username', 'my_password', url) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_cp4d_token_manager.py python-ibm-cloud-sdk-core-3.12.0/test/test_cp4d_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_cp4d_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_cp4d_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,13 +1,17 @@ # pylint: disable=missing-docstring import json +import time -import responses import jwt +import responses + from ibm_cloud_sdk_core import CP4DTokenManager + @responses.activate def test_request_token(): url = "https://test" + now = time.time() access_token_layout = { "username": "dummy", "role": "Admin", @@ -19,31 +23,34 @@ "iss": "sss", "aud": "sss", "uid": "sss", - "iat": 1559324664, - "exp": 1559324664 + "iat": now, + "exp": now + 3600 } access_token = jwt.encode(access_token_layout, 'secret', algorithm='HS256', - headers={'kid': '230498151c214b788dd97f22b85410a5'}).decode('utf-8') + headers={'kid': '230498151c214b788dd97f22b85410a5'}) response = { - "accessToken": access_token, - "token_type": "Bearer", - "expires_in": 3600, - "expiration": 1524167011, - "refresh_token": "jy4gl91BQ" + "token": access_token, } - responses.add(responses.GET, url + '/v1/preauth/validateAuth', body=json.dumps(response), status=200) + responses.add(responses.POST, url + '/v1/authorize', body=json.dumps(response), status=200) token_manager = CP4DTokenManager("username", "password", url) token_manager.set_disable_ssl_verification(True) token = token_manager.get_token() - assert responses.calls[0].request.url == url + '/v1/preauth/validateAuth' + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == url + '/v1/authorize' assert token == access_token - token_manager = CP4DTokenManager("username", "password", url + '/v1/preauth/validateAuth') + token_manager = CP4DTokenManager("username", "password", url + '/v1/authorize') token = token_manager.get_token() - assert responses.calls[0].request.url == url + '/v1/preauth/validateAuth' - assert token == access_token assert len(responses.calls) == 2 + assert responses.calls[1].request.url == url + '/v1/authorize' + assert token == access_token + + token_manager = CP4DTokenManager(username="username", apikey="fake_api_key", url=url + '/v1/authorize') + token = token_manager.get_token() + assert len(responses.calls) == 3 + assert responses.calls[2].request.url == url + '/v1/authorize' + assert token == access_token diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_detailed_response.py python-ibm-cloud-sdk-core-3.12.0/test/test_detailed_response.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_detailed_response.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_detailed_response.py 2021-10-15 20:30:50.000000000 +0000 @@ -4,12 +4,15 @@ import responses import requests + from ibm_cloud_sdk_core import DetailedResponse + def clean(val): """Eliminate all whitespace and convert single to double quotes""" return val.translate(str.maketrans('', '', ' \n\t\r')).replace("'", "\"") + @responses.activate def test_detailed_response_dict(): responses.add(responses.GET, @@ -19,10 +22,11 @@ content_type='application/json') mock_response = requests.get('https://test.com') - detailed_response = DetailedResponse(mock_response.json(), mock_response.headers, mock_response.status_code) + detailed_response = DetailedResponse(response=mock_response.json(), headers=mock_response.headers, + status_code=mock_response.status_code) assert detailed_response is not None assert detailed_response.get_result() == {'foobar': 'baz'} - assert detailed_response.get_headers() == {u'Content-Type': 'application/json'} + assert detailed_response.get_headers() == {'Content-Type': 'application/json'} assert detailed_response.get_status_code() == 200 response_str = clean(detailed_response.__str__()) @@ -30,6 +34,7 @@ #assert clean(detailed_response.get_headers().__str__()) in response_str assert clean(detailed_response.get_status_code().__str__()) in response_str + @responses.activate def test_detailed_response_list(): responses.add(responses.GET, @@ -39,10 +44,11 @@ content_type='application/json') mock_response = requests.get('https://test.com') - detailed_response = DetailedResponse(mock_response.json(), mock_response.headers, mock_response.status_code) + detailed_response = DetailedResponse(response=mock_response.json(), headers=mock_response.headers, + status_code=mock_response.status_code) assert detailed_response is not None assert detailed_response.get_result() == ['foobar', 'baz'] - assert detailed_response.get_headers() == {u'Content-Type': 'application/json'} + assert detailed_response.get_headers() == {'Content-Type': 'application/json'} assert detailed_response.get_status_code() == 200 response_str = clean(detailed_response.__str__()) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_iam_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_iam_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_iam_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_iam_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,28 +1,33 @@ # pylint: disable=missing-docstring import json +import jwt import pytest import responses -import jwt -from ibm_cloud_sdk_core.authenticators import IAMAuthenticator +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator, Authenticator def test_iam_authenticator(): - authenticator = IAMAuthenticator('my_apikey') + authenticator = IAMAuthenticator(apikey='my_apikey') assert authenticator is not None - assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com/identity/token' + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM + assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None assert authenticator.token_manager.apikey == 'my_apikey' + assert authenticator.token_manager.scope is None authenticator.set_client_id_and_secret('tom', 'jerry') assert authenticator.token_manager.client_id == 'tom' assert authenticator.token_manager.client_secret == 'jerry' + authenticator.set_scope('scope1 scope2 scope3') + assert authenticator.token_manager.scope == 'scope1 scope2 scope3' + with pytest.raises(TypeError) as err: authenticator.set_headers('dummy') assert str(err.value) == 'headers must be a dictionary' @@ -37,6 +42,38 @@ authenticator.set_proxies({'dummy': 'proxies'}) assert authenticator.token_manager.proxies == {'dummy': 'proxies'} + authenticator.set_disable_ssl_verification(True) + assert authenticator.token_manager.disable_ssl_verification + + +def test_disable_ssl_verification(): + authenticator = IAMAuthenticator( + apikey='my_apikey', disable_ssl_verification=True) + assert authenticator.token_manager.disable_ssl_verification is True + + authenticator.set_disable_ssl_verification(False) + assert authenticator.token_manager.disable_ssl_verification is False + + +def test_invalid_disable_ssl_verification_type(): + with pytest.raises(TypeError) as err: + authenticator = IAMAuthenticator( + apikey='my_apikey', disable_ssl_verification='True') + assert str(err.value) == 'disable_ssl_verification must be a bool' + + authenticator = IAMAuthenticator(apikey='my_apikey') + assert authenticator.token_manager.disable_ssl_verification is False + + with pytest.raises(TypeError) as err: + authenticator.set_disable_ssl_verification('True') + assert str(err.value) == 'status must be a bool' + + +def test_iam_authenticator_with_scope(): + authenticator = IAMAuthenticator(apikey='my_apikey', scope='scope1 scope2') + assert authenticator is not None + assert authenticator.token_manager.scope == 'scope1 scope2' + def test_iam_authenticator_validate_failed(): with pytest.raises(ValueError) as err: @@ -83,7 +120,6 @@ headers={ 'kid': '230498151c214b788dd97f22b85410a5' }) - access_token = access_token.decode('utf-8') response = { "access_token": access_token, "token_type": "Bearer", @@ -94,7 +130,28 @@ responses.add( responses.POST, url=url, body=json.dumps(response), status=200) - authenticator = IAMAuthenticator('my_apikey') + auth_headers = {'Host': 'iam.cloud.ibm.com:443'} + authenticator = IAMAuthenticator('my_apikey', headers=auth_headers) + + # Simulate an SDK API request that needs to be authenticated. request = {'headers': {}} + + # Trigger the "get token" processing to obtain the access token and add to the "SDK request". authenticator.authenticate(request) + + # Verify that the "authenticate()" method added the Authorization header assert request['headers']['Authorization'] is not None + + # Verify that the "get token" call contained the Host header. + assert responses.calls[0].request.headers.get( + 'Host') == 'iam.cloud.ibm.com:443' + + +def test_multiple_iam_authenticators(): + authenticator_1 = IAMAuthenticator('my_apikey') + assert authenticator_1.token_manager.request_payload['apikey'] == 'my_apikey' + + authenticator_2 = IAMAuthenticator('my_other_apikey') + assert authenticator_2.token_manager.request_payload['apikey'] == 'my_other_apikey' + + assert authenticator_1.token_manager.request_payload['apikey'] == 'my_apikey' diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_iam_token_manager.py python-ibm-cloud-sdk-core-3.12.0/test/test_iam_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_iam_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_iam_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,13 +1,15 @@ # pylint: disable=missing-docstring +import os import time -import responses import jwt import pytest +import responses + +from ibm_cloud_sdk_core import IAMTokenManager, ApiException, get_authenticator_from_environment -from ibm_cloud_sdk_core import IAMTokenManager, ApiException -def get_access_token(): +def get_access_token() -> str: access_token_layout = { "username": "dummy", "role": "Admin", @@ -25,7 +27,8 @@ access_token = jwt.encode(access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'}) - return access_token.decode('utf-8') + return access_token + @responses.activate def test_request_token_auth_default(): @@ -47,6 +50,7 @@ assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response + @responses.activate def test_request_token_auth_in_ctor(): iam_url = "https://iam.cloud.ibm.com/identity/token" @@ -60,13 +64,40 @@ default_auth_header = 'Basic Yng6Yng=' responses.add(responses.POST, url=iam_url, body=response, status=200) - token_manager = IAMTokenManager("apikey", iam_url, 'foo', 'bar') + token_manager = IAMTokenManager( + "apikey", url=iam_url, client_id='foo', client_secret='bar') + token_manager.request_token() + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == iam_url + assert responses.calls[0].request.headers['Authorization'] != default_auth_header + assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + + +@responses.activate +def test_request_token_auth_in_ctor_with_scope(): + iam_url = "https://iam.cloud.ibm.com/identity/token" + response = """{ + "access_token": "oAeisG8yqPY7sFR_x66Z15", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": 1524167011, + "refresh_token": "jy4gl91BQ" + }""" + default_auth_header = 'Basic Yng6Yng=' + responses.add(responses.POST, url=iam_url, body=response, status=200) + + token_manager = IAMTokenManager( + "apikey", url=iam_url, client_id='foo', client_secret='bar', scope='john snow') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert responses.calls[0].response.text == response + assert 'scope=john+snow' in responses.calls[0].response.request.body + @responses.activate def test_request_token_unsuccessful(): @@ -100,6 +131,7 @@ assert responses.calls[0].request.url == iam_url assert responses.calls[0].response.text == response + @responses.activate def test_request_token_auth_in_ctor_client_id_only(): iam_url = "https://iam.cloud.ibm.com/identity/token" @@ -112,13 +144,15 @@ }""" responses.add(responses.POST, url=iam_url, body=response, status=200) - token_manager = IAMTokenManager("iam_apikey", iam_url, 'foo') + token_manager = IAMTokenManager("iam_apikey", url=iam_url, client_id='foo') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + @responses.activate def test_request_token_auth_in_ctor_secret_only(): @@ -132,13 +166,16 @@ }""" responses.add(responses.POST, url=iam_url, body=response, status=200) - token_manager = IAMTokenManager("iam_apikey", iam_url, None, 'bar') + token_manager = IAMTokenManager( + "iam_apikey", url=iam_url, client_id=None, client_secret='bar') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + @responses.activate def test_request_token_auth_in_setter(): @@ -161,6 +198,8 @@ assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + @responses.activate def test_request_token_auth_in_setter_client_id_only(): @@ -182,6 +221,8 @@ assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + @responses.activate def test_request_token_auth_in_setter_secret_only(): @@ -197,10 +238,125 @@ token_manager = IAMTokenManager("iam_apikey") token_manager.set_client_id_and_secret(None, 'bar') - token_manager.set_headers({'user':'header'}) + token_manager.set_headers({'user': 'header'}) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response + assert 'scope' not in responses.calls[0].response.request.body + + +@responses.activate +def test_request_token_auth_in_setter_scope(): + iam_url = "https://iam.cloud.ibm.com/identity/token" + response = """{ + "access_token": "oAeisG8yqPY7sFR_x66Z15", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": 1524167011, + "refresh_token": "jy4gl91BQ" + }""" + responses.add(responses.POST, url=iam_url, body=response, status=200) + + token_manager = IAMTokenManager("iam_apikey") + token_manager.set_client_id_and_secret(None, 'bar') + token_manager.set_headers({'user': 'header'}) + token_manager.set_scope('john snow') + token_manager.request_token() + + assert len(responses.calls) == 1 + assert responses.calls[0].request.url == iam_url + assert responses.calls[0].request.headers.get('Authorization') is None + assert responses.calls[0].response.text == response + assert 'scope=john+snow' in responses.calls[0].response.request.body + + +@responses.activate +def test_get_refresh_token(): + iam_url = "https://iam.cloud.ibm.com/identity/token" + access_token_str = get_access_token() + response = """{ + "access_token": "%s", + "token_type": "Bearer", + "expires_in": 3600, + "expiration": 1524167011, + "refresh_token": "jy4gl91BQ" + }""" % (access_token_str) + responses.add(responses.POST, url=iam_url, body=response, status=200) + + token_manager = IAMTokenManager("iam_apikey") + token_manager.get_token() + + assert len(responses.calls) == 2 + assert token_manager.refresh_token == "jy4gl91BQ" + +# +# In order to run the following integration test with a live IAM server: +# +# 1. Create file "iamtest.env" in the project root. +# It should look like this: +# IAMTEST1_AUTH_URL= e.g. https://iam.cloud.ibm.com +# IAMTEST1_AUTH_TYPE=iam +# IAMTEST1_APIKEY= +# IAMTEST2_AUTH_URL= e.g. https://iam.test.cloud.ibm.com +# IAMTEST2_AUTH_TYPE=iam +# IAMTEST2_APIKEY= +# IAMTEST2_CLIENT_ID= +# IAMTEST2_CLIENT_SECRET= +# +# 2. Comment out the "@pytest.mark.skip" decorator below. +# +# 3. Run this command: +# python3 -m pytest -s test -k "test_iam_live_token_server" +# (or just run tests like normal and this test function will be invoked) +# + + +@pytest.mark.skip(reason="avoid integration test in automated builds") +def test_iam_live_token_server(): + # Get two iam authenticators from the environment. + # "iamtest1" uses the production IAM token server + # "iamtest2" uses the staging IAM token server + os.environ['IBM_CREDENTIALS_FILE'] = "iamtest.env" + + # Test "iamtest1" service + auth1 = get_authenticator_from_environment("iamtest1") + assert auth1 is not None + assert auth1.token_manager is not None + assert auth1.token_manager.url is not None + + request = {'method': "GET"} + request["url"] = "" + request["headers"] = {} + + assert auth1.token_manager.refresh_token is None + + auth1.authenticate(request) + + assert request.get("headers") is not None + assert request["headers"].get("Authorization") is not None + assert "Bearer " in request["headers"].get("Authorization") + + # Test "iamtest2" service + auth2 = get_authenticator_from_environment("iamtest2") + assert auth2 is not None + assert auth2.token_manager is not None + assert auth2.token_manager.url is not None + + request = {'method': "GET"} + request["url"] = "" + request["headers"] = {} + + assert auth2.token_manager.refresh_token is None + + auth2.authenticate(request) + + assert auth2.token_manager.refresh_token is not None + + assert request.get("headers") is not None + assert request["headers"].get("Authorization") is not None + assert "Bearer " in request["headers"].get("Authorization") + + # print('Refresh token: ', auth2.token_manager.refresh_token) diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_jwt_token_manager.py python-ibm-cloud-sdk-core-3.12.0/test/test_jwt_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_jwt_token_manager.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_jwt_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,18 +1,23 @@ -# pylint: disable=missing-docstring,protected-access +# pylint: disable=missing-docstring,protected-access,abstract-class-instantiated import time +import threading +from typing import Optional + import jwt import pytest -from ibm_cloud_sdk_core import JWTTokenManager +from ibm_cloud_sdk_core import JWTTokenManager, DetailedResponse + class JWTTokenManagerMockImpl(JWTTokenManager): - def __init__(self, url=None, access_token=None): + def __init__(self, url: Optional[str] = None, access_token: Optional[str] = None) -> None: self.url = url self.access_token = access_token self.request_count = 0 # just for tests to see how many times request was called - super(JWTTokenManagerMockImpl, self).__init__(url, access_token, 'access_token') + super().__init__(url, disable_ssl_verification=access_token, + token_name='access_token') - def request_token(self): + def request_token(self) -> DetailedResponse: self.request_count += 1 current_time = int(time.time()) token_layout = { @@ -35,19 +40,22 @@ response = {"access_token": access_token, "token_type": "Bearer", "expires_in": 3600, - "expiration": current_time, + "expiration": current_time + 3600, "refresh_token": "jy4gl91BQ", "from_token_manager": True } + time.sleep(0.5) return response -def _get_current_time(): + +def _get_current_time() -> int: return int(time.time()) + def test_get_token(): url = "https://iam.cloud.ibm.com/identity/token" token_manager = JWTTokenManagerMockImpl(url) - token = token_manager.get_token() + old_token = token_manager.get_token() assert token_manager.token_info.get('expires_in') == 3600 assert token_manager._is_token_expired() is False @@ -58,27 +66,42 @@ "refresh_token": "jy4gl91BQ" } token = token_manager.get_token() - assert token == "old_dummy" + assert token == old_token # expired token: - token_manager.time_for_new_token = _get_current_time() - 300 + token_manager.expire_time = _get_current_time() - 300 token = token_manager.get_token() assert token != "old_dummy" assert token_manager.request_count == 2 + +def test_paced_get_token(): + url = "https://iam.cloud.ibm.com/identity/token" + token_manager = JWTTokenManagerMockImpl(url) + threads = [] + for _ in range(10): + thread = threading.Thread(target=token_manager.get_token) + thread.start() + threads.append(thread) + for thread in threads: + thread.join() + assert token_manager.request_count == 1 + + def test_is_token_expired(): - token_manager = JWTTokenManagerMockImpl(None, None) + token_manager = JWTTokenManagerMockImpl(None, access_token=None) assert token_manager._is_token_expired() is True - token_manager.time_for_new_token = _get_current_time() + 3600 + token_manager.expire_time = _get_current_time() + 3600 assert token_manager._is_token_expired() is False - token_manager.time_for_new_token = _get_current_time() - 3600 + token_manager.expire_time = _get_current_time() - 3600 assert token_manager._is_token_expired() -def test_not_implemented_error(): - with pytest.raises(NotImplementedError) as err: - token_manager = JWTTokenManager(None, None) - token_manager.request_token() - assert str(err.value) == 'request_token MUST be overridden by a subclass of JWTTokenManager.' + +def test_abstract_class_instantiation(): + with pytest.raises(TypeError) as err: + JWTTokenManager(None) + assert str(err.value).startswith("Can't instantiate abstract class JWTTokenManager with abstract") + def test_disable_ssl_verification(): token_manager = JWTTokenManagerMockImpl('https://iam.cloud.ibm.com/identity/token') diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_no_auth_authenticator.py python-ibm-cloud-sdk-core-3.12.0/test/test_no_auth_authenticator.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_no_auth_authenticator.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_no_auth_authenticator.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,11 +1,12 @@ # pylint: disable=missing-docstring -from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator +from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator, Authenticator + def test_no_auth_authenticator(): authenticator = NoAuthAuthenticator() assert authenticator is not None - assert authenticator.authentication_type == 'noAuth' + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_NOAUTH authenticator.validate() diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_token_manager.py python-ibm-cloud-sdk-core-3.12.0/test/test_token_manager.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_token_manager.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_token_manager.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,84 @@ +# coding: utf-8 + +# Copyright 2020 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=missing-docstring,protected-access,abstract-class-instantiated +from types import SimpleNamespace +from unittest import mock + +import pytest + +from ibm_cloud_sdk_core import ApiException +from ibm_cloud_sdk_core.token_managers.token_manager import TokenManager + + +class MockTokenManager(TokenManager): + + def request_token(self) -> None: + response = self._request( + method='GET', + url=self.url + ) + return response + + def _save_token_info(self, token_response: dict) -> None: + pass + + +def test_abstract_class_instantiation(): + with pytest.raises(TypeError) as err: + TokenManager(None) + assert str(err.value) == "Can't instantiate abstract class " \ + "TokenManager with abstract methods " \ + "_save_token_info, " \ + "request_token" + + +def requests_request_spy(*args, **kwargs): + return SimpleNamespace(status_code=200, request_args=args, request_kwargs=kwargs) + + +@mock.patch('requests.request', side_effect=requests_request_spy) +def test_request_passes_disable_ssl_verification(request): # pylint: disable=unused-argument + mock_token_manager = MockTokenManager(url="https://example.com", disable_ssl_verification=True) + assert mock_token_manager.request_token().request_kwargs['verify'] is False + + +def requests_request_error_mock(*args, **kwargs): # pylint: disable=unused-argument + return SimpleNamespace(status_code=300, headers={}, text="") + + +@mock.patch('requests.request', side_effect=requests_request_error_mock) +def test_request_raises_for_non_2xx(request): # pylint: disable=unused-argument + mock_token_manager = MockTokenManager(url="https://example.com", disable_ssl_verification=True) + with pytest.raises(ApiException): + mock_token_manager.request_token() + + +def test_set_disable_ssl_verification_success(): + token_manager = MockTokenManager(None) + assert token_manager.disable_ssl_verification is False + + token_manager.set_disable_ssl_verification(True) + assert token_manager.disable_ssl_verification is True + + +def test_set_disable_ssl_verification_fail(): + token_manager = MockTokenManager(None) + + with pytest.raises(TypeError) as err: + token_manager.set_disable_ssl_verification('True') + assert str(err.value) == 'status must be a bool' + assert token_manager.disable_ssl_verification is False diff -Nru python-ibm-cloud-sdk-core-1.6.2/test/test_utils.py python-ibm-cloud-sdk-core-3.12.0/test/test_utils.py --- python-ibm-cloud-sdk-core-1.6.2/test/test_utils.py 2020-02-13 21:41:59.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test/test_utils.py 2021-10-15 20:30:50.000000000 +0000 @@ -1,11 +1,86 @@ # pylint: disable=missing-docstring +# coding: utf-8 + +# Copyright 2019, 2021 IBM All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import datetime import os +from typing import Optional + +import pytest from ibm_cloud_sdk_core import string_to_datetime, datetime_to_string, get_authenticator_from_environment +from ibm_cloud_sdk_core import string_to_datetime_list, datetime_to_string_list from ibm_cloud_sdk_core import string_to_date, date_to_string from ibm_cloud_sdk_core import convert_model, convert_list -from ibm_cloud_sdk_core.authenticators import BasicAuthenticator, IAMAuthenticator +from ibm_cloud_sdk_core import get_query_param +from ibm_cloud_sdk_core import read_external_sources +from ibm_cloud_sdk_core.authenticators import Authenticator, BasicAuthenticator, IAMAuthenticator + + +def datetime_test(datestr: str, expected: str): + dt_value = string_to_datetime(datestr) + assert dt_value is not None + actual = datetime_to_string(dt_value) + assert actual == expected + + +def test_datetime(): + # RFC 3339 with various flavors of tz-offset + datetime_test('2016-06-20T04:25:16.218Z', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16.218+0000', + '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16.218+00', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16.218-0000', + '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16.218-00', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T00:25:16.218-0400', + '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T00:25:16.218-04', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T07:25:16.218+0300', + '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T07:25:16.218+03', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16Z', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T04:25:16+0000', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T04:25:16-0000', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T01:25:16-0300', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T01:25:16-03:00', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T08:55:16+04:30', '2016-06-20T04:25:16Z') + datetime_test('2016-06-20T16:25:16+12:00', '2016-06-20T04:25:16Z') + + # RFC 3339 with nanoseconds for the Catalog-Managements of the world. + datetime_test('2020-03-12T10:52:12.866305005-04:00', + '2020-03-12T14:52:12.866305Z') + datetime_test('2020-03-12T10:52:12.866305005Z', + '2020-03-12T10:52:12.866305Z') + datetime_test('2020-03-12T10:52:12.866305005+02:30', + '2020-03-12T08:22:12.866305Z') + datetime_test('2020-03-12T10:52:12.866305Z', '2020-03-12T10:52:12.866305Z') + + # UTC datetime with no TZ. + datetime_test('2016-06-20T04:25:16.218', '2016-06-20T04:25:16.218000Z') + datetime_test('2016-06-20T04:25:16', '2016-06-20T04:25:16Z') + + # Dialog datetime. + datetime_test('2016-06-20 04:25:16', '2016-06-20T04:25:16Z') + + # IAM Identity Service. + datetime_test('2020-11-10T12:28+0000', '2020-11-10T12:28:00Z') + datetime_test('2020-11-10T07:28-0500', '2020-11-10T12:28:00Z') + datetime_test('2020-11-10T12:28Z', '2020-11-10T12:28:00Z') + def test_string_to_datetime(): # If the specified string does not include a timezone, it is assumed to be UTC @@ -17,13 +92,14 @@ date = string_to_datetime('2017-03-06 16:00:04.159338+0600') assert date.day == 6 assert date.hour == 16 - assert date.tzinfo.utcoffset(None).total_seconds() == 6*60*60 + assert date.tzinfo.utcoffset(None).total_seconds() == 6 * 60 * 60 # Test date string with TZ specified as 'Z' date = string_to_datetime('2017-03-06 16:00:04.159338Z') assert date.day == 6 assert date.hour == 16 assert date.tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) + def test_datetime_to_string(): # If specified date is None, return None assert datetime_to_string(None) is None @@ -32,7 +108,8 @@ res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' # Test date with UTC timezone - date = datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, datetime.timezone.utc) + date = datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, + datetime.timezone.utc) res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' # Test date with non-UTC timezone @@ -41,6 +118,72 @@ res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' + +def test_string_to_datetime_list(): + # Assert ValueError is raised for invalid argument type + with pytest.raises(ValueError): + string_to_datetime_list(None) + # If the specified string does not include a timezone, it is assumed to be UTC + date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338']) + assert date_list[0].day == 6 + assert date_list[0].hour == 16 + assert date_list[0].tzinfo.utcoffset( + None) == datetime.timezone.utc.utcoffset(None) + # Test date string with TZ specified as '+xxxx' + date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338+0600']) + assert date_list[0].day == 6 + assert date_list[0].hour == 16 + assert date_list[0].tzinfo.utcoffset(None).total_seconds() == 6 * 60 * 60 + # Test date string with TZ specified as 'Z' + date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338Z']) + assert date_list[0].day == 6 + assert date_list[0].hour == 16 + assert date_list[0].tzinfo.utcoffset( + None) == datetime.timezone.utc.utcoffset(None) + # Test multiple datetimes in a list + date_list = string_to_datetime_list( + ['2017-03-06 16:00:04.159338', '2017-03-07 17:00:04.159338']) + assert date_list[0].day == 6 + assert date_list[0].hour == 16 + assert date_list[0].tzinfo.utcoffset( + None) == datetime.timezone.utc.utcoffset(None) + assert date_list[1].day == 7 + assert date_list[1].hour == 17 + assert date_list[1].tzinfo.utcoffset( + None) == datetime.timezone.utc.utcoffset(None) + + +def test_datetime_to_string_list(): + # Assert ValueError is raised for invalid argument type + with pytest.raises(ValueError): + datetime_to_string_list(None) + # If specified datetime list item is None, return list of None + assert datetime_to_string_list([None]) == [None] + # If specified datetime list is empty, return empty list + assert datetime_to_string_list([]) == [] + # If the specified date list item is "naive", it is interpreted as a UTC date + date_list = [datetime.datetime(2017, 3, 6, 16, 0, 4, 159338)] + res = datetime_to_string_list(date_list) + assert res == ['2017-03-06T16:00:04.159338Z'] + # Test date list item with UTC timezone + date_list = [datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, + datetime.timezone.utc)] + res = datetime_to_string_list(date_list) + assert res == ['2017-03-06T16:00:04.159338Z'] + # Test date list item with non-UTC timezone + tzn = datetime.timezone(datetime.timedelta(hours=-6)) + date_list = [datetime.datetime(2017, 3, 6, 10, 0, 4, 159338, tzn)] + res = datetime_to_string_list(date_list) + assert res == ['2017-03-06T16:00:04.159338Z'] + # Test specified date list with multiple items + date_list = [datetime.datetime(2017, 3, 6, 16, 0, 4, 159338), + datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, + datetime.timezone.utc)] + res = datetime_to_string_list(date_list) + assert res == ['2017-03-06T16:00:04.159338Z', + '2017-03-06T16:00:04.159338Z'] + + def test_date_conversion(): date = string_to_date('2017-03-06') assert date.day == 6 @@ -48,14 +191,55 @@ assert res == '2017-03-06' assert date_to_string(None) is None -def test_convert_model(): - class MockModel: +def test_get_query_param(): + # Relative URL + next_url = '/api/v1/offerings?start=foo&limit=10' + page_token = get_query_param(next_url, 'start') + assert page_token == 'foo' + # Absolute URL + next_url = 'https://acme.com/api/v1/offerings?start=bar&limit=10' + page_token = get_query_param(next_url, 'start') + assert page_token == 'bar' + # Missing param + next_url = 'https://acme.com/api/v1/offerings?start=bar&limit=10' + page_token = get_query_param(next_url, 'token') + assert page_token is None + # No URL + page_token = get_query_param(None, 'start') + assert page_token is None + # Empty URL + page_token = get_query_param('', 'start') + assert page_token is None + # No query string + next_url = '/api/v1/offerings' + page_token = get_query_param(next_url, 'start') + assert page_token is None + # Bad query string + next_url = '/api/v1/offerings?start%XXfoo' + with pytest.raises(ValueError): + page_token = get_query_param(next_url, 'start') + # Duplicate param + next_url = '/api/v1/offerings?start=foo&start=bar&limit=10' + page_token = get_query_param(next_url, 'start') + assert page_token == 'foo' + # Bad URL - since the behavior for this case varies based on the version of Python + # we allow _either_ a ValueError or that the illegal chars are just ignored + next_url = 'https://foo.bar\u2100/api/v1/offerings?start=foo' + try: + page_token = get_query_param(next_url, 'start') + assert page_token == 'foo' + except ValueError: + # This is okay. + pass - def __init__(self, xyz=None): + +def test_convert_model(): + class MockModel: + def __init__(self, xyz: Optional[str] = None) -> None: self.xyz = xyz - def to_dict(self): + def to_dict(self) -> dict: _dict = {} if hasattr(self, 'xyz') and self.xyz is not None: _dict['xyz'] = self.xyz @@ -77,6 +261,7 @@ mock3_dict = convert_model(mock3) assert mock3_dict == mock3 + def test_convert_list(): temp = ['default', '123'] res_str = convert_list(temp) @@ -94,54 +279,151 @@ mock4_str = convert_list(mock4) assert mock4_str == mock4 + +# pylint: disable=too-many-statements def test_get_authenticator_from_credential_file(): - file_path = os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-iam.env') + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-iam.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('ibm watson') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' + assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' + assert authenticator.token_manager.client_id is None + assert authenticator.token_manager.client_secret is None + assert authenticator.token_manager.disable_ssl_verification is False + assert authenticator.token_manager.scope is None del os.environ['IBM_CREDENTIALS_FILE'] - file_path = os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-basic.env') + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-basic.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'my_username' del os.environ['IBM_CREDENTIALS_FILE'] - file_path = os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-cp4d.env') + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-container.env') + os.environ['IBM_CREDENTIALS_FILE'] = file_path + authenticator = get_authenticator_from_environment('service 1') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER + assert authenticator.token_manager.cr_token_filename == 'crtoken.txt' + assert authenticator.token_manager.iam_profile_name == 'iam-user-123' + assert authenticator.token_manager.iam_profile_id == 'iam-id-123' + assert authenticator.token_manager.url == 'https://iamhost/iam/api' + assert authenticator.token_manager.scope == 'scope1' + assert authenticator.token_manager.client_id == 'iam-client-123' + assert authenticator.token_manager.client_secret == 'iam-secret-123' + assert authenticator.token_manager.disable_ssl_verification is True + + authenticator = get_authenticator_from_environment('service 2') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER + assert authenticator.token_manager.cr_token_filename is None + assert authenticator.token_manager.iam_profile_name == 'iam-user-123' + assert authenticator.token_manager.iam_profile_id is None + assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' + assert authenticator.token_manager.scope is None + assert authenticator.token_manager.client_id is None + assert authenticator.token_manager.client_secret is None + assert authenticator.token_manager.disable_ssl_verification is False + del os.environ['IBM_CREDENTIALS_FILE'] + + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-cp4d.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CP4D assert authenticator.token_manager.username == 'my_username' assert authenticator.token_manager.password == 'my_password' + assert authenticator.token_manager.url == 'https://my_url/v1/authorize' + assert authenticator.token_manager.apikey is None + assert authenticator.token_manager.disable_ssl_verification is False del os.environ['IBM_CREDENTIALS_FILE'] - file_path = os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-no-auth.env') + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-no-auth.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_NOAUTH del os.environ['IBM_CREDENTIALS_FILE'] - file_path = os.path.join( - os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env') + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials-bearer.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BEARERTOKEN assert authenticator.bearer_token is not None del os.environ['IBM_CREDENTIALS_FILE'] + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials.env') + os.environ['IBM_CREDENTIALS_FILE'] = file_path + authenticator = get_authenticator_from_environment('service_1') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM + assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' + assert authenticator.token_manager.client_id == 'somefake========id' + assert authenticator.token_manager.client_secret == '==my-client-secret==' + assert authenticator.token_manager.url == 'https://iamhost/iam/api=' + assert authenticator.token_manager.scope is None + del os.environ['IBM_CREDENTIALS_FILE'] + + +def test_get_authenticator_from_credential_file_scope(): + file_path = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials.env') + os.environ['IBM_CREDENTIALS_FILE'] = file_path + authenticator = get_authenticator_from_environment('service_2') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM + assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' + assert authenticator.token_manager.client_id == 'somefake========id' + assert authenticator.token_manager.client_secret == '==my-client-secret==' + assert authenticator.token_manager.url == 'https://iamhost/iam/api=' + assert authenticator.token_manager.scope == 'A B C D' + del os.environ['IBM_CREDENTIALS_FILE'] + + def test_get_authenticator_from_env_variables(): os.environ['TEST_APIKEY'] = '5678efgh' authenticator = get_authenticator_from_environment('test') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' del os.environ['TEST_APIKEY'] + os.environ['TEST_IAM_PROFILE_ID'] = 'iam-profile-id1' + authenticator = get_authenticator_from_environment('test') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER + assert authenticator.token_manager.iam_profile_id == 'iam-profile-id1' + del os.environ['TEST_IAM_PROFILE_ID'] + + os.environ['SERVICE_1_APIKEY'] = 'V4HXmoUtMjohnsnow=KotN' + authenticator = get_authenticator_from_environment('service_1') + assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM + assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' + del os.environ['SERVICE_1_APIKEY'] + + os.environ['SERVICE_2_APIKEY'] = 'johnsnow' + os.environ['SERVICE_2_SCOPE'] = 'A B C D' + authenticator = get_authenticator_from_environment('service_2') + assert authenticator is not None + assert authenticator.token_manager.apikey == 'johnsnow' + assert authenticator.token_manager.scope == 'A B C D' + del os.environ['SERVICE_2_APIKEY'] + del os.environ['SERVICE_2_SCOPE'] + + def test_vcap_credentials(): vcap_services = '{"test":[{"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ @@ -151,6 +433,7 @@ os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, BasicAuthenticator) + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] @@ -162,6 +445,7 @@ os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, IAMAuthenticator) + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'bogus apikey' del os.environ['VCAP_SERVICES'] @@ -172,6 +456,7 @@ os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, IAMAuthenticator) + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'bogus apikey' del os.environ['VCAP_SERVICES'] @@ -184,10 +469,12 @@ os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('testname') assert isinstance(authenticator, BasicAuthenticator) + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] + def test_vcap_credentials_2(): vcap_services = '{\ "test":[{"name": "testname",\ @@ -204,13 +491,26 @@ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ - "password":"bogus password"}}]}' + "password":"bogus password"}}],\ + "equals_sign_test":[{"name": "equals_sign_test",\ + "credentials":{ \ + "iam_apikey": "V4HXmoUtMjohnsnow=KotN",\ + "iam_apikey_description": "Auto generated apikey...",\ + "iam_apikey_name": "auto-generated-apikey-111-222-333",\ + "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",\ + "iam_serviceid_crn": "crn:v1:staging:public:iam-identity::a/::serviceid:ServiceID-1234",\ + "url": "https://gateway.watsonplatform.net/testService",\ + "auth_url": "https://iamhost/iam/api="}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('testname') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.username == 'bogus username2' assert authenticator.password == 'bogus password2' + + authenticator = get_authenticator_from_environment('equals_sign_test') + assert isinstance(authenticator, IAMAuthenticator) + assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[{\ @@ -258,5 +558,31 @@ os.environ['PERSONALITY_INSIGHTS_APIKEY'] = '5678efgh' authenticator = get_authenticator_from_environment('personality-insights') assert authenticator is not None + assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' del os.environ['PERSONALITY_INSIGHTS_APIKEY'] + + +def test_read_external_sources_1(): + # Set IBM_CREDENTIALS_FILE to a non-existent file (should be silently ignored). + bad_file_path = os.path.join(os.path.dirname(__file__), 'NOT_A_FILE') + os.environ['IBM_CREDENTIALS_FILE'] = bad_file_path + + # This env var should take precendence since the config file wasn't found. + os.environ['SERVICE_1_URL'] = 'https://good-url.com' + + config = read_external_sources('service_1') + assert config.get('URL') == 'https://good-url.com' + + +def test_read_external_sources_2(): + # The config file should take precedence over the env variable. + config_file = os.path.join(os.path.dirname(__file__), + '../resources/ibm-credentials.env') + os.environ['IBM_CREDENTIALS_FILE'] = config_file + + # This should be ignored since IBM_CREDENTIALS_FILE points to a valid file. + os.environ['SERVICE_1_URL'] = 'wrong-url' + + config = read_external_sources('service_1') + assert config.get('URL') == 'service1.com/api' diff -Nru python-ibm-cloud-sdk-core-1.6.2/test_integration/test_cp4d_authenticator_integration.py python-ibm-cloud-sdk-core-3.12.0/test_integration/test_cp4d_authenticator_integration.py --- python-ibm-cloud-sdk-core-1.6.2/test_integration/test_cp4d_authenticator_integration.py 1970-01-01 00:00:00.000000000 +0000 +++ python-ibm-cloud-sdk-core-3.12.0/test_integration/test_cp4d_authenticator_integration.py 2021-10-15 20:30:50.000000000 +0000 @@ -0,0 +1,45 @@ +# pylint: disable=missing-docstring +import os + +from ibm_cloud_sdk_core import get_authenticator_from_environment + +# Note: Only the unit tests are run by default. +# +# In order to test with a live CP4D server, rename "ibm-credentials-cp4dtest.env.example" to +# "ibm-credentials-cp4dtest.env" in the resources folder and populate the fields. +# Then run this command: +# pytest test_integration + +IBM_CREDENTIALS_FILE = '../resources/ibm-credentials-cp4dtest.env' + + +def test_cp4d_authenticator_password(): + file_path = os.path.join( + os.path.dirname(__file__), IBM_CREDENTIALS_FILE) + os.environ['IBM_CREDENTIALS_FILE'] = file_path + + authenticator = get_authenticator_from_environment('cp4d_password_test') + assert authenticator is not None + assert authenticator.token_manager.password is not None + assert authenticator.token_manager.apikey is None + + request = {'headers': {}} + authenticator.authenticate(request) + assert request['headers']['Authorization'] is not None + assert 'Bearer' in request['headers']['Authorization'] + + +def test_cp4d_authenticator_apikey(): + file_path = os.path.join( + os.path.dirname(__file__), IBM_CREDENTIALS_FILE) + os.environ['IBM_CREDENTIALS_FILE'] = file_path + + authenticator = get_authenticator_from_environment('cp4d_apikey_test') + assert authenticator is not None + assert authenticator.token_manager.password is None + assert authenticator.token_manager.apikey is not None + + request = {'headers': {}} + authenticator.authenticate(request) + assert request['headers']['Authorization'] is not None + assert 'Bearer' in request['headers']['Authorization']