From 5f8601116efc7a14dea8a9c4bd4f1a965d7e4a8d Mon Sep 17 00:00:00 2001 From: Robert Veitch Date: Tue, 16 Jul 2019 10:30:20 -0500 Subject: [PATCH] Release 1.1.0 --- CHANGELOG.md | 5 + cos_config/__init__.py | 2 +- cos_config/common.py | 69 +++ cos_config/iam_token_manager.py | 154 ------- cos_config/resource_configuration_v1.py | 189 ++++++-- cos_config/version.py | 1 - cos_config/watson_service.py | 589 ------------------------ requirements.txt | 5 +- setup.py | 6 +- 9 files changed, 233 insertions(+), 787 deletions(-) create mode 100644 cos_config/common.py delete mode 100644 cos_config/iam_token_manager.py delete mode 100644 cos_config/version.py delete mode 100755 cos_config/watson_service.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1168376..df2fb83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +# 1.1.0 +## Content +### Features +* Activity Tracker + # 1.0.1 ## Content ### Defect Fixes diff --git a/cos_config/__init__.py b/cos_config/__init__.py index 1b37fb1..fa6f011 100644 --- a/cos_config/__init__.py +++ b/cos_config/__init__.py @@ -1,2 +1,2 @@ __author__ = 'IBM' -__version__ = '1.0.2.dev1' \ No newline at end of file +__version__ = '1.1.0' diff --git a/cos_config/common.py b/cos_config/common.py new file mode 100644 index 0000000..3ae1ced --- /dev/null +++ b/cos_config/common.py @@ -0,0 +1,69 @@ + +# 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 platform +from .__init__ import __version__ + +SDK_ANALYTICS_HEADER = 'X-IBMCloud-SDK-Analytics' +USER_AGENT_HEADER = 'User-Agent' +SDK_NAME = 'ibm-cos-resource-config-sdk-python/' + __version__ + +def get_system_info(): + """ + return system information + :return: + """ + return '{0} {1} {2}'.format(# OS + platform.system(), + # OS version + platform.release(), + # Python version + platform.python_version()) + +def get_user_agent(): + """ + return user agent + :return: + """ + return user_agent + +def get_sdk_analytics(service_name, service_version, operation_id): + """ + return analytics + + :param service_name: + :param service_version: + :param operation_id: + :return: + """ + return 'service_name={0};service_version={1};operation_id={2}'.format( + service_name, service_version, operation_id) + +user_agent = '{0}-{1} {2}'.format(SDK_NAME, __version__, get_system_info()) + +def get_sdk_headers(service_name, service_version, operation_id): + """ + return headers + :param service_name: + :param service_version: + :param operation_id: + :return: + """ + headers = {} + headers[SDK_ANALYTICS_HEADER] = get_sdk_analytics(service_name, service_version, operation_id) + headers[USER_AGENT_HEADER] = get_user_agent() + return headers diff --git a/cos_config/iam_token_manager.py b/cos_config/iam_token_manager.py deleted file mode 100644 index 74cb9a1..0000000 --- a/cos_config/iam_token_manager.py +++ /dev/null @@ -1,154 +0,0 @@ -import requests -import time - -DEFAULT_IAM_URL = 'https://iam.bluemix.net/identity/token' -CONTENT_TYPE = 'application/x-www-form-urlencoded' -ACCEPT = 'application/json' -DEFAULT_AUTHORIZATION = 'Basic Yng6Yng=' -REQUEST_TOKEN_GRANT_TYPE = 'urn:ibm:params:oauth:grant-type:apikey' -REQUEST_TOKEN_RESPONSE_TYPE = 'cloud_iam' -REFRESH_TOKEN_GRANT_TYPE = 'refresh_token' - -class IAMTokenManager(object): - def __init__(self, iam_apikey=None, iam_access_token=None, iam_url=None): - self.iam_apikey = iam_apikey - self.user_access_token = iam_access_token - self.iam_url = iam_url if iam_url else DEFAULT_IAM_URL - self.token_info = { - 'access_token': None, - 'refresh_token': None, - 'token_type': None, - 'expires_in': None, - 'expiration': None, - } - - def request(self, method, url, headers=None, params=None, data=None, **kwargs): - response = requests.request(method=method, url=url, - headers=headers, params=params, - data=data, **kwargs) - if 200 <= response.status_code <= 299: - return response.json() - else: - from .watson_service import WatsonApiException, get_error_message - error_message = get_error_message(response) - raise WatsonApiException(response.status_code, message=error_message, httpResponse=response) - - def get_token(self): - """ - The source of the token is determined by the following logic: - 1. If user provides their own managed access token, assume it is valid and send it - 2. If this class is managing tokens and does not yet have one, make a request for one - 3. If this class is managing tokens and the token has expired refresh it. In case the refresh token is expired, get a new one - If this class is managing tokens and has a valid token stored, send it - """ - if self.user_access_token: - return self.user_access_token - elif not self.token_info.get('access_token'): - token_info = self._request_token() - self._save_token_info(token_info) - return self.token_info.get('access_token') - elif self._is_token_expired(): - if self._is_refresh_token_expired(): - token_info = self._request_token() - else: - token_info = self._refresh_token() - self._save_token_info(token_info) - return self.token_info.get('access_token') - else: - return self.token_info.get('access_token') - - def _request_token(self): - """ - Request an IAM token using an API key - """ - headers = { - 'Content-type': CONTENT_TYPE, - 'Authorization': DEFAULT_AUTHORIZATION, - 'accept': ACCEPT - } - data = { - 'grant_type': REQUEST_TOKEN_GRANT_TYPE, - 'apikey': self.iam_apikey, - 'response_type': REQUEST_TOKEN_RESPONSE_TYPE - } - response = self.request( - method='POST', - url=self.iam_url, - headers=headers, - data=data) - return response - - def _refresh_token(self): - """ - Refresh an IAM token using a refresh token - """ - headers = { - 'Content-type': CONTENT_TYPE, - 'Authorization': DEFAULT_AUTHORIZATION, - 'accept': ACCEPT - } - data = { - 'grant_type': REFRESH_TOKEN_GRANT_TYPE, - 'refresh_token': self.token_info.get('refresh_token') - } - response = self.request( - method='POST', - url=self.iam_url, - headers=headers, - data=data) - return response - - def set_access_token(self, iam_access_token): - """ - Set a self-managed IAM access token. - The access token should be valid and not yet expired. - """ - self.user_access_token = iam_access_token - - def set_iam_apikey(self, iam_apikey): - """ - Set the IAM api key - """ - self.iam_apikey = iam_apikey - - def set_iam_url(self, iam_url): - """ - Set the IAM url - """ - self.iam_url = iam_url - - def _is_token_expired(self): - """ - Check if currently stored token is expired. - - Using a buffer to prevent the edge case of the - oken expiring before the request could be made. - - The buffer will be a fraction of the total TTL. Using 80%. - """ - fraction_of_ttl = 0.8 - time_to_live = self.token_info.get('expires_in') - expire_time = self.token_info.get('expiration') - refresh_time = expire_time - (time_to_live * (1.0 - fraction_of_ttl)) - current_time = int(time.time()) - return refresh_time < current_time - - def _is_refresh_token_expired(self): - """ - Used as a fail-safe to prevent the condition of a refresh token expiring, - which could happen after around 30 days. This function will return true - if it has been at least 7 days and 1 hour since the last token was set - """ - if self.token_info.get('expiration') is None: - return True - - seven_days = 7 * 24 * 3600 - current_time = int(time.time()) - new_token_time = self.token_info.get('expiration') + seven_days - return new_token_time < current_time - - def _save_token_info(self, token_info): - """ - Save the response from the IAM service request to the object's state. - """ - self.token_info = token_info diff --git a/cos_config/resource_configuration_v1.py b/cos_config/resource_configuration_v1.py index b349a1c..6c86ea5 100644 --- a/cos_config/resource_configuration_v1.py +++ b/cos_config/resource_configuration_v1.py @@ -22,14 +22,15 @@ from __future__ import absolute_import import json -from .watson_service import datetime_to_string, string_to_datetime -from .watson_service import WatsonService +from .common import get_sdk_headers +from ibm_cloud_sdk_core import BaseService +from ibm_cloud_sdk_core import datetime_to_string, string_to_datetime ############################################################################## # Service ############################################################################## -class ResourceConfigurationV1(WatsonService): +class ResourceConfigurationV1(BaseService): """The ResourceConfiguration V1 service.""" default_url = 'https://config.cloud-object-storage.cloud.ibm.com/v1' @@ -39,13 +40,15 @@ def __init__(self, iam_apikey=None, iam_access_token=None, iam_url=None, + iam_client_id=None, + iam_client_secret=None, ): """ Construct a new client for the ResourceConfiguration service. :param str url: The base url to use when contacting the service (e.g. "https://config.cloud-object-storage.cloud.ibm.com/v1/v1"). - The base url may differ between Bluemix regions. + The base url may differ between IBM Cloud regions. :param str iam_apikey: An API key that can be used to request IAM tokens. If this API key is provided, the SDK will manage the token and handle the @@ -57,17 +60,23 @@ def __init__(self, made with an expired token will fail. :param str iam_url: An optional URL for the IAM service API. Defaults to - 'https://iam.bluemix.net/identity/token'. + 'https://iam.cloud.ibm.com/identity/token'. + + :param str iam_client_id: An optional client_id value to use when interacting with the IAM service. + + :param str iam_client_secret: An optional client_secret value to use when interacting with the IAM service. """ - WatsonService.__init__(self, - vcap_services_name='', - url=url, - iam_apikey=iam_apikey, - iam_access_token=iam_access_token, - iam_url=iam_url, - use_vcap_services=True, - display_name='ResourceConfiguration') + BaseService.__init__(self, + vcap_services_name='resource_configuration', + url=url, + iam_apikey=iam_apikey, + iam_access_token=iam_access_token, + iam_url=iam_url, + iam_client_id=iam_client_id, + iam_client_secret=iam_client_secret, + use_vcap_services=True, + display_name='ResourceConfiguration') ######################### # buckets @@ -92,7 +101,8 @@ def get_bucket_config(self, bucket, **kwargs): } if 'headers' in kwargs: headers.update(kwargs.get('headers')) - headers['X-IBMCloud-SDK-Analytics'] = 'service_name=;service_version=V1;operation_id=get_bucket_config' + sdk_headers = get_sdk_headers('resource_configuration', 'V1', 'get_bucket_config') + headers.update(sdk_headers) url = '/b/{0}'.format(*self._encode_path_vars(bucket)) response = self.request(method='GET', @@ -102,7 +112,7 @@ def get_bucket_config(self, bucket, **kwargs): return response - def update_bucket_config(self, bucket, firewall=None, if_match=None, **kwargs): + def update_bucket_config(self, bucket, firewall=None, activity_tracking=None, if_match=None, **kwargs): """ Make changes to a bucket's configuration. @@ -115,10 +125,15 @@ def update_bucket_config(self, bucket, firewall=None, if_match=None, **kwargs): number of objects in a bucket, any timestamps, or other non-mutable fields. :param str bucket: Name of a bucket. - :param Firewall firewall: A filter that controls access based on the network where - request originated. Requests not originating from IP addresses listed in the - `allowed_ip` field will be denied. Viewing or updating the `Firewall` element - requires the requester to have the `manager` role. + :param Firewall firewall: An access control mechanism based on the network (IP + address) where request originated. Requests not originating from IP addresses + listed in the `allowed_ip` field will be denied regardless of any access policies + (including public access) that might otherwise permit the request. Viewing or + updating the `Firewall` element requires the requester to have the `manager` role. + :param ActivityTracking activity_tracking: Enables sending log data to Activity + Tracker and LogDNA to provide visibility into object read and write events. All + object events are sent to the activity tracker instance defined in the + `activity_tracker_crn` field. :param str if_match: An Etag previously returned in a header when fetching or updating a bucket's metadata. If this value does not match the active Etag, the request will fail. @@ -131,16 +146,20 @@ def update_bucket_config(self, bucket, firewall=None, if_match=None, **kwargs): raise ValueError('bucket must be provided') if firewall is not None: firewall = self._convert_model(firewall, Firewall) + if activity_tracking is not None: + activity_tracking = self._convert_model(activity_tracking, ActivityTracking) headers = { 'if-match': if_match } if 'headers' in kwargs: headers.update(kwargs.get('headers')) - headers['X-IBMCloud-SDK-Analytics'] = 'service_name=;service_version=V1;operation_id=update_bucket_config' + sdk_headers = get_sdk_headers('resource_configuration', 'V1', 'update_bucket_config') + headers.update(sdk_headers) data = { - 'firewall': firewall + 'firewall': firewall, + 'activity_tracking': activity_tracking } url = '/b/{0}'.format(*self._encode_path_vars(bucket)) @@ -148,7 +167,7 @@ def update_bucket_config(self, bucket, firewall=None, if_match=None, **kwargs): url=url, headers=headers, json=data, - accept_json=True) + accept_json=False) return response @@ -158,6 +177,81 @@ def update_bucket_config(self, bucket, firewall=None, if_match=None, **kwargs): ############################################################################## +class ActivityTracking(object): + """ + Enables sending log data to Activity Tracker and LogDNA to provide visibility into + object read and write events. All object events are sent to the activity tracker + instance defined in the `activity_tracker_crn` field. + + :attr bool read_data_events: (optional) If set to `true`, all object read events (i.e. + downloads) will be sent to Activity Tracker. + :attr bool write_data_events: (optional) If set to `true`, all object write events + (i.e. uploads) will be sent to Activity Tracker. + :attr str activity_tracker_crn: (optional) Required the first time `activity_tracking` + is configured. The instance of Activity Tracker that will recieve object event data. + The format is "crn:v1:bluemix:public:logdnaat:{bucket location}:a/{storage + account}:{activity tracker service instance}::". + """ + + def __init__(self, read_data_events=None, write_data_events=None, activity_tracker_crn=None): + """ + Initialize a ActivityTracking object. + + :param bool read_data_events: (optional) If set to `true`, all object read events + (i.e. downloads) will be sent to Activity Tracker. + :param bool write_data_events: (optional) If set to `true`, all object write + events (i.e. uploads) will be sent to Activity Tracker. + :param str activity_tracker_crn: (optional) Required the first time + `activity_tracking` is configured. The instance of Activity Tracker that will + recieve object event data. The format is "crn:v1:bluemix:public:logdnaat:{bucket + location}:a/{storage account}:{activity tracker service instance}::". + """ + self.read_data_events = read_data_events + self.write_data_events = write_data_events + self.activity_tracker_crn = activity_tracker_crn + + @classmethod + def _from_dict(cls, _dict): + """Initialize a ActivityTracking object from a json dictionary.""" + args = {} + validKeys = ['read_data_events', 'write_data_events', 'activity_tracker_crn'] + badKeys = set(_dict.keys()) - set(validKeys) + if badKeys: + raise ValueError('Unrecognized keys detected in dictionary for class ActivityTracking: ' + ', '.join(badKeys)) + if 'read_data_events' in _dict: + args['read_data_events'] = _dict.get('read_data_events') + if 'write_data_events' in _dict: + args['write_data_events'] = _dict.get('write_data_events') + if 'activity_tracker_crn' in _dict: + args['activity_tracker_crn'] = _dict.get('activity_tracker_crn') + return cls(**args) + + def _to_dict(self): + """Return a json dictionary representing this model.""" + _dict = {} + if hasattr(self, 'read_data_events') and self.read_data_events is not None: + _dict['read_data_events'] = self.read_data_events + if hasattr(self, 'write_data_events') and self.write_data_events is not None: + _dict['write_data_events'] = self.write_data_events + if hasattr(self, 'activity_tracker_crn') and self.activity_tracker_crn is not None: + _dict['activity_tracker_crn'] = self.activity_tracker_crn + return _dict + + def __str__(self): + """Return a `str` version of this ActivityTracking object.""" + return json.dumps(self._to_dict(), indent=2) + + def __eq__(self, other): + """Return `true` when self and other are equal, false otherwise.""" + if not isinstance(other, self.__class__): + return False + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Return `true` when self and other are not equal, false otherwise.""" + return not self == other + + class Bucket(object): """ A bucket. @@ -174,13 +268,18 @@ class Bucket(object): 3339 format. Non-mutable. :attr int object_count: (optional) Total number of objects in the bucket. Non-mutable. :attr int bytes_used: (optional) Total size of all objects in the bucket. Non-mutable. - :attr Firewall firewall: (optional) A filter that controls access based on the network - where request originated. Requests not originating from IP addresses listed in the - `allowed_ip` field will be denied. Viewing or updating the `Firewall` element - requires the requester to have the `manager` role. + :attr Firewall firewall: (optional) An access control mechanism based on the network + (IP address) where request originated. Requests not originating from IP addresses + listed in the `allowed_ip` field will be denied regardless of any access policies + (including public access) that might otherwise permit the request. Viewing or + updating the `Firewall` element requires the requester to have the `manager` role. + :attr ActivityTracking activity_tracking: (optional) Enables sending log data to + Activity Tracker and LogDNA to provide visibility into object read and write events. + All object events are sent to the activity tracker instance defined in the + `activity_tracker_crn` field. """ - def __init__(self, name=None, crn=None, service_instance_id=None, service_instance_crn=None, time_created=None, time_updated=None, object_count=None, bytes_used=None, firewall=None): + def __init__(self, name=None, crn=None, service_instance_id=None, service_instance_crn=None, time_created=None, time_updated=None, object_count=None, bytes_used=None, firewall=None, activity_tracking=None): """ Initialize a Bucket object. @@ -199,10 +298,16 @@ def __init__(self, name=None, crn=None, service_instance_id=None, service_instan Non-mutable. :param int bytes_used: (optional) Total size of all objects in the bucket. Non-mutable. - :param Firewall firewall: (optional) A filter that controls access based on the - network where request originated. Requests not originating from IP addresses - listed in the `allowed_ip` field will be denied. Viewing or updating the - `Firewall` element requires the requester to have the `manager` role. + :param Firewall firewall: (optional) An access control mechanism based on the + network (IP address) where request originated. Requests not originating from IP + addresses listed in the `allowed_ip` field will be denied regardless of any access + policies (including public access) that might otherwise permit the request. + Viewing or updating the `Firewall` element requires the requester to have the + `manager` role. + :param ActivityTracking activity_tracking: (optional) Enables sending log data to + Activity Tracker and LogDNA to provide visibility into object read and write + events. All object events are sent to the activity tracker instance defined in the + `activity_tracker_crn` field. """ self.name = name self.crn = crn @@ -213,11 +318,16 @@ def __init__(self, name=None, crn=None, service_instance_id=None, service_instan self.object_count = object_count self.bytes_used = bytes_used self.firewall = firewall + self.activity_tracking = activity_tracking @classmethod def _from_dict(cls, _dict): """Initialize a Bucket object from a json dictionary.""" args = {} + validKeys = ['name', 'crn', 'service_instance_id', 'service_instance_crn', 'time_created', 'time_updated', 'object_count', 'bytes_used', 'firewall', 'activity_tracking'] + badKeys = set(_dict.keys()) - set(validKeys) + if badKeys: + raise ValueError('Unrecognized keys detected in dictionary for class Bucket: ' + ', '.join(badKeys)) if 'name' in _dict: args['name'] = _dict.get('name') if 'crn' in _dict: @@ -236,6 +346,8 @@ def _from_dict(cls, _dict): args['bytes_used'] = _dict.get('bytes_used') if 'firewall' in _dict: args['firewall'] = Firewall._from_dict(_dict.get('firewall')) + if 'activity_tracking' in _dict: + args['activity_tracking'] = ActivityTracking._from_dict(_dict.get('activity_tracking')) return cls(**args) def _to_dict(self): @@ -259,6 +371,8 @@ def _to_dict(self): _dict['bytes_used'] = self.bytes_used if hasattr(self, 'firewall') and self.firewall is not None: _dict['firewall'] = self.firewall._to_dict() + if hasattr(self, 'activity_tracking') and self.activity_tracking is not None: + _dict['activity_tracking'] = self.activity_tracking._to_dict() return _dict def __str__(self): @@ -278,10 +392,11 @@ def __ne__(self, other): class Firewall(object): """ - A filter that controls access based on the network where request originated. Requests - not originating from IP addresses listed in the `allowed_ip` field will be denied. - Viewing or updating the `Firewall` element requires the requester to have the - `manager` role. + An access control mechanism based on the network (IP address) where request + originated. Requests not originating from IP addresses listed in the `allowed_ip` + field will be denied regardless of any access policies (including public access) that + might otherwise permit the request. Viewing or updating the `Firewall` element + requires the requester to have the `manager` role. :attr list[str] allowed_ip: (optional) List of IPv4 or IPv6 addresses in CIDR notation to be affected by firewall in CIDR notation is supported. Passing an empty array will @@ -304,6 +419,10 @@ def __init__(self, allowed_ip=None): def _from_dict(cls, _dict): """Initialize a Firewall object from a json dictionary.""" args = {} + validKeys = ['allowed_ip'] + badKeys = set(_dict.keys()) - set(validKeys) + if badKeys: + raise ValueError('Unrecognized keys detected in dictionary for class Firewall: ' + ', '.join(badKeys)) if 'allowed_ip' in _dict: args['allowed_ip'] = _dict.get('allowed_ip') return cls(**args) diff --git a/cos_config/version.py b/cos_config/version.py deleted file mode 100644 index 220ce64..0000000 --- a/cos_config/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '1.0.2.dev1' diff --git a/cos_config/watson_service.py b/cos_config/watson_service.py deleted file mode 100755 index e887bf0..0000000 --- a/cos_config/watson_service.py +++ /dev/null @@ -1,589 +0,0 @@ -# coding: utf-8 -# Copyright 2017 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 as json_import -import platform -import os -from os.path import dirname, isfile, join, expanduser, abspath -import requests -import sys -from requests.structures import CaseInsensitiveDict -import dateutil.parser as date_parser -from .iam_token_manager import IAMTokenManager -import warnings - -try: - from http.cookiejar import CookieJar # Python 3 -except ImportError: - from cookielib import CookieJar # Python 2 -from .version import __version__ - -BEARER = 'Bearer' -X_WATSON_AUTHORIZATION_TOKEN = 'X-Watson-Authorization-Token' -AUTH_HEADER_DEPRECATION_MESSAGE = 'Authenticating with the X-Watson-Authorization-Token header is deprecated. The token continues to work with Cloud Foundry services, but is not supported for services that use Identity and Access Management (IAM) authentication.' -ICP_PREFIX = 'icp-' -APIKEY = 'apikey' -URL = 'url' -USERNAME = 'username' -PASSWORD = 'password' -IAM_APIKEY = 'iam_apikey' -IAM_URL = 'iam_url' -APIKEY_DEPRECATION_MESSAGE = 'Authenticating with apikey is deprecated. Move to using Identity and Access Management (IAM) authentication.' -DEFAULT_CREDENTIALS_FILE_NAME = 'ibm-credentials.env' - -# Uncomment this to enable http debugging -# try: -# import http.client as http_client -# except ImportError: -# # Python 2 -# import httplib as http_client -# http_client.HTTPConnection.debuglevel = 1 - - -def load_from_vcap_services(service_name): - vcap_services = os.getenv("VCAP_SERVICES") - if vcap_services is not None: - services = json_import.loads(vcap_services) - if service_name in services: - return services[service_name][0]["credentials"] - else: - return None - -class WatsonException(Exception): - """ - Custom exception class for Watson Services. - """ - pass - - -class WatsonApiException(WatsonException): - """ - Custom exception class for errors returned from Watson APIs. - - :param int code: The HTTP status code returned. - :param str message: A message describing the error. - :param dict info: A dictionary of additional information about the error. - :param response httpResponse: response - """ - def __init__(self, code, message, info=None, httpResponse=None): - # Call the base class constructor with the parameters it needs - super(WatsonApiException, self).__init__(message) - self.message = message - self.code = code - self.info = info - self.httpResponse = httpResponse - self.transactionId = None - self.globalTransactionId = None - if httpResponse is not None: - self.transactionId = httpResponse.headers.get('X-DP-Watson-Tran-ID') - self.globalTransactionId = httpResponse.headers.get('X-Global-Transaction-ID') - - - def __str__(self): - msg = 'Error: ' + str(self.message) + ', Code: ' + str(self.code) - if self.info is not None: - msg += ' , Information: ' + str(self.info) - if self.transactionId is not None: - msg += ' , X-dp-watson-tran-id: ' + str(self.transactionId) - if self.globalTransactionId is not None: - msg += ' , X-global-transaction-id: ' + str(self.globalTransactionId) - return msg - - -class WatsonInvalidArgument(WatsonException): - pass - -def datetime_to_string(datetime): - """ - Serializes a datetime to a string. - :param datetime: datetime value - :return: string. containing iso8601 format date string - """ - return datetime.isoformat().replace('+00:00', 'Z') - - -def string_to_datetime(string): - """ - Deserializes string to datetime. - :param string: string containing datetime in iso8601 format - :return: datetime. - """ - return date_parser.parse(string) - - -def _cleanup_value(value): - if isinstance(value, bool): - return 'true' if value else 'false' - return value - - -def _cleanup_values(dictionary): - if isinstance(dictionary, dict): - return dict( - [(k, _cleanup_value(v)) for k, v in dictionary.items()]) - return dictionary - - -def _remove_null_values(dictionary): - if isinstance(dictionary, dict): - return dict([(k, v) for k, v in dictionary.items() if v is not None]) - return dictionary - - -def _convert_boolean_value(value): - if isinstance(value, bool): - return 1 if value else 0 - return value - - -def _convert_boolean_values(dictionary): - if isinstance(dictionary, dict): - return dict( - [(k, _convert_boolean_value(v)) for k, v in dictionary.items()]) - return dictionary - -def _has_bad_first_or_last_char(str): - return str is not None and (str.startswith('{') or str.startswith('"') or str.endswith('}') or str.endswith('"')) - -def get_error_message(response): - """ - Gets the error message from a JSON response. - :return: the error message - :rtype: string - """ - error_message = 'Unknown error' - try: - error_json = response.json() - if 'error' in error_json: - if isinstance(error_json['error'], dict) and 'description' in \ - error_json['error']: - error_message = error_json['error']['description'] - else: - error_message = error_json['error'] - elif 'error_message' in error_json: - error_message = error_json['error_message'] - elif 'errorMessage' in error_json: - error_message = error_json['errorMessage'] - elif 'msg' in error_json: - error_message = error_json['msg'] - elif 'message' in error_json: - error_message = error_json['message'] - elif 'statusInfo' in error_json: - error_message = error_json['statusInfo'] - return error_message - except: - return response.text or error_message - -class DetailedResponse(object): - """ - Custom class for detailed response returned from Watson APIs. - - :param Response response: Either json response or http Response as requested. - :param dict headers: A dict of response headers - :param str status_code: HTTP response code - """ - def __init__(self, response=None, headers=None, status_code=None): - self.result = response - self.headers = headers - self.status_code = status_code - - def get_result(self): - return self.result - - def get_headers(self): - return self.headers - - def get_status_code(self): - return self.status_code - - def _to_dict(self): - _dict = {} - if hasattr(self, 'result') and self.result is not None: - _dict['result'] = self.result if isinstance(self.result, dict) else 'HTTP response' - if hasattr(self, 'headers') and self.headers is not None: - _dict['headers'] = self.headers - if hasattr(self, 'status_code') and self.status_code is not None: - _dict['status_code'] = self.status_code - return _dict - - def __str__(self): - return json_import.dumps(self._to_dict(), indent=4, default=lambda o: o.__dict__) - -class WatsonService(object): - def __init__(self, vcap_services_name, url, username=None, password=None, - use_vcap_services=True, api_key=None, - iam_apikey=None, iam_access_token=None, iam_url=None, - display_name=None): - """ - Loads credentials from the VCAP_SERVICES environment variable if - available, preferring credentials explicitly - set in the request. - If VCAP_SERVICES is not found (or use_vcap_services is set to False), - username and password credentials must - be specified. - """ - - self.url = url - self.jar = None - self.api_key = None - self.username = None - self.password = None - self.default_headers = None - self.http_config = {} - self.detailed_response = True - self.iam_apikey = None - self.iam_access_token = None - self.iam_url = None - self.token_manager = None - self.verify = None # Indicates whether to ignore verifying the SSL certification - - if _has_bad_first_or_last_char(self.url): - raise ValueError('The URL shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your URL') - - user_agent_string = 'ibm-cos-resource-config-sdk-python-' + __version__ # SDK version - user_agent_string += ' ' + platform.system() # OS - user_agent_string += ' ' + platform.release() # OS version - user_agent_string += ' ' + platform.python_version() # Python version - self.user_agent_header = {'user-agent': user_agent_string} - - # 1. Credentials are passed in constructor - if api_key is not None: - self.set_api_key(api_key) - elif username is not None and password is not None: - if username is APIKEY and not password.startswith(ICP_PREFIX): - self.set_token_manager(password, iam_access_token, iam_url) - else: - self.set_username_and_password(username, password) - elif iam_access_token is not None or iam_apikey is not None: - if iam_apikey and iam_apikey.startswith(ICP_PREFIX): - self.set_username_and_password(APIKEY, iam_apikey) - else: - self.set_token_manager(iam_apikey, iam_access_token, iam_url) - - # 2. Credentials from credential file - if display_name and not self.username and not self.token_manager: - service_name = display_name.replace(' ', '_').lower() - self.load_from_credential_file(service_name) - - # 3. Credentials from VCAP - if use_vcap_services and not self.username and not self.token_manager: - self.vcap_service_credentials = load_from_vcap_services( - vcap_services_name) - if self.vcap_service_credentials is not None and isinstance( - self.vcap_service_credentials, dict): - self.url = self.vcap_service_credentials['url'] - if 'username' in self.vcap_service_credentials: - self.username = self.vcap_service_credentials.get('username') - if 'password' in self.vcap_service_credentials: - self.password = self.vcap_service_credentials.get('password') - if 'apikey' in self.vcap_service_credentials: - self.set_iam_apikey(self.vcap_service_credentials.get('apikey')) - if 'iam_apikey' in self.vcap_service_credentials: - self.set_iam_apikey(self.vcap_service_credentials.get('iam_apikey')) - if 'iam_access_token' in self.vcap_service_credentials: - self.set_iam_access_token(self.vcap_service_credentials.get('iam_access_token')) - - if (self.username is None or self.password is None)\ - and self.api_key is None and self.token_manager is None: - raise ValueError( - 'You must specify your IAM api key or username and password service ' - 'credentials (Note: these are different from your Bluemix id)') - - def load_from_credential_file(self, service_name, separator='='): - """ - Initiates the credentials based on the credential file - - :param str service_name: The service name - :param str separator: the separator for key value pair - """ - # File path specified by an env variable - credential_file_path = os.getenv("IBM_CREDENTIALS_FILE") - - # Home directory - if credential_file_path is None: - file_path = join(expanduser('~'), DEFAULT_CREDENTIALS_FILE_NAME) - if isfile(file_path): - credential_file_path = file_path - - # Top-level of the project directory - if credential_file_path is None: - file_path = join(dirname(dirname(abspath(__file__))), DEFAULT_CREDENTIALS_FILE_NAME) - if isfile(file_path): - credential_file_path = file_path - - if credential_file_path is not None: - with open(credential_file_path, 'r') as fp: - for line in fp: - key_val = line.strip().split(separator) - if len(key_val) == 2: - self._set_credential_based_on_type(service_name, key_val[0].lower(), key_val[1].lower()) - - - def _set_credential_based_on_type(self, service_name, key, value): - if service_name in key: - if APIKEY in key: - self.set_iam_apikey(value) - elif URL in key: - self.set_url(value) - elif USERNAME in key: - self.username = value - elif PASSWORD in key: - self.password = value - elif IAM_APIKEY in key: - self.set_iam_apikey(value) - elif IAM_URL in key: - self.set_iam_url(value) - - def set_username_and_password(self, username=None, password=None): - if username == 'YOUR SERVICE USERNAME': - username = None - if password == 'YOUR SERVICE PASSWORD': - password = None - - if _has_bad_first_or_last_char(username): - raise ValueError('The username shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your username') - if _has_bad_first_or_last_char(password): - raise ValueError('The password shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your password') - - self.username = username - self.password = password - self.jar = CookieJar() - - def set_api_key(self, api_key): - if api_key is not None: - warnings.warn(APIKEY_DEPRECATION_MESSAGE) - if api_key == 'YOUR API KEY': - api_key = None - if api_key is not None and api_key.startswith(ICP_PREFIX): - self.set_username_and_password(APIKEY, api_key) - return - - self.api_key = api_key - - # This would be called only for Visual recognition - if self.url is self.default_url: - self.set_url('https://gateway-a.watsonplatform.net/visual-recognition/api') - self.jar = CookieJar() - - def set_token_manager(self, iam_apikey=None, iam_access_token=None, iam_url=None): - if iam_apikey == 'YOUR IAM API KEY': - return - if _has_bad_first_or_last_char(iam_apikey): - raise ValueError('The credentials shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your credentials') - self.iam_apikey = iam_apikey - self.iam_access_token = iam_access_token - self.iam_url = iam_url - self.token_manager = IAMTokenManager(iam_apikey, iam_access_token, iam_url) - self.jar = CookieJar() - - def set_iam_access_token(self, iam_access_token): - if self.token_manager: - self.token_manager.set_access_token(iam_access_token) - else: - self.token_manager = IAMTokenManager(iam_access_token=iam_access_token) - self.iam_access_token = iam_access_token - self.jar = CookieJar() - - def set_iam_url(self, iam_url): - if self.token_manager: - self.token_manager.set_iam_url(iam_url) - else: - self.token_manager = IAMTokenManager(iam_url=iam_url) - self.iam_url = iam_url - self.jar = CookieJar() - - def set_iam_apikey(self, iam_apikey): - if _has_bad_first_or_last_char(iam_apikey): - raise ValueError('The credentials shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your credentials') - if self.token_manager: - self.token_manager.set_iam_apikey(iam_apikey) - else: - self.token_manager = IAMTokenManager(iam_apikey=iam_apikey) - self.iam_apikey = iam_apikey - self.jar = CookieJar() - - def set_url(self, url): - if _has_bad_first_or_last_char(url): - raise ValueError('The URL shouldn\'t start or end with curly brackets or quotes. ' - 'Be sure to remove any {} and \" characters surrounding your URL') - self.url = url - - def set_default_headers(self, headers): - """ - Set http headers to be sent in every request. - :param headers: A dictionary of header names and values - """ - if isinstance(headers, dict): - self.default_headers = headers - else: - raise TypeError("headers parameter must be a dictionary") - - def set_http_config(self, http_config): - """ - Sets the http client config like timeout, proxies, etc. - """ - if isinstance(http_config, dict): - self.http_config = http_config - else: - raise TypeError("http_config parameter must be a dictionary") - - def disable_SSL_verification(self): - self.verify = False - - def set_detailed_response(self, detailed_response): - self.detailed_response = detailed_response - - # Could make this compute the label_id based on the variable name of the - # dictionary passed in (using **kwargs), but - # this might be confusing to understand. - @staticmethod - def unpack_id(dictionary, label_id): - if isinstance(dictionary, dict) and label_id in dictionary: - return dictionary[label_id] - return dictionary - - @staticmethod - def _convert_model(val, classname=None): - if classname is not None and not hasattr(val, "_from_dict"): - if isinstance(val, str): - val = json_import.loads(val) - val = classname._from_dict(dict(val)) - if hasattr(val, "_to_dict"): - return val._to_dict() - return val - - @staticmethod - def _convert_list(val): - if isinstance(val, list): - return ",".join(val) - return val - - @staticmethod - def _encode_path_vars(*args): - return (requests.utils.quote(x, safe='') for x in args) - - @staticmethod - def _get_error_info(response): - """ - Gets the error info (if any) from a JSON response. - :return: A `dict` containing additional information about the error. - :rtype: dict - """ - info_keys = ['code_description', 'description', 'errors', 'help', - 'sub_code', 'warnings'] - error_info = {} - try: - error_json = response.json() - error_info = {k:v for k, v in error_json.items() if k in info_keys} - except: - pass - return error_info if any(error_info) else None - - - def request(self, method, url, accept_json=False, headers=None, - params=None, json=None, data=None, files=None, **kwargs): - full_url = self.url + url - input_headers = _remove_null_values(headers) if headers else {} - input_headers = _cleanup_values(input_headers) - - headers = CaseInsensitiveDict(self.user_agent_header) - if self.default_headers is not None: - headers.update(self.default_headers) - if accept_json: - headers['accept'] = 'application/json' - headers.update(input_headers) - - if X_WATSON_AUTHORIZATION_TOKEN in headers: - warnings.warn(AUTH_HEADER_DEPRECATION_MESSAGE) - - # Remove keys with None values - params = _remove_null_values(params) - params = _cleanup_values(params) - json = _remove_null_values(json) - data = _remove_null_values(data) - files = _remove_null_values(files) - - if sys.version_info >= (3, 0) and isinstance(data, str): - data = data.encode('utf-8') - - # Support versions of requests older than 2.4.2 without the json input - if not data and json is not None: - data = json_import.dumps(json) - headers.update({'content-type': 'application/json'}) - - auth = None - if self.token_manager: - access_token = self.token_manager.get_token() - headers['Authorization'] = '{0} {1}'.format(BEARER, access_token) - if self.username and self.password: - auth = (self.username, self.password) - if self.api_key is not None: - if params is None: - params = {} - if full_url.startswith( - 'https://gateway-a.watsonplatform.net/calls'): - params['apikey'] = self.api_key - else: - params['api_key'] = self.api_key - - # Use a one minute timeout when our caller doesn't give a timeout. - # http://docs.python-requests.org/en/master/user/quickstart/#timeouts - kwargs = dict({"timeout": 60}, **kwargs) - kwargs = dict(kwargs, **self.http_config) - - if self.verify is not None: - kwargs['verify'] = self.verify - - response = requests.request(method=method, url=full_url, - cookies=self.jar, auth=auth, - headers=headers, - params=params, data=data, files=files, - **kwargs) - - if 200 <= response.status_code <= 299: - if response.status_code == 204 or method == 'HEAD': - # There is no body content for a HEAD request or a 204 response - return DetailedResponse(None, response.headers, response.status_code) if self.detailed_response else None - if accept_json: - try: - response_json = response.json() - except: - # deserialization fails because there is no text - return DetailedResponse(None, response.headers, response.status_code) if self.detailed_response else None - if 'status' in response_json and response_json['status'] \ - == 'ERROR': - status_code = 400 - error_message = 'Unknown error' - - if 'statusInfo' in response_json: - error_message = response_json['statusInfo'] - if error_message == 'invalid-api-key': - status_code = 401 - raise WatsonApiException(status_code, error_message, httpResponse=response) - return DetailedResponse(response_json, response.headers, response.status_code) if self.detailed_response else response_json - return DetailedResponse(response, response.headers, response.status_code) if self.detailed_response else response - else: - if response.status_code == 401: - error_message = 'Unauthorized: Access is denied due to ' \ - 'invalid credentials ' - else: - error_message = get_error_message(response) - error_info = self._get_error_info(response) - raise WatsonApiException(response.status_code, error_message, - info=error_info, httpResponse=response) diff --git a/requirements.txt b/requirements.txt index 8995e7c..f96191a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ -python-dateutil==2.7.5 requests==2.21.0 -Requires==0.0.3 -six==1.12.0 -urllib3==1.24.1 \ No newline at end of file +ibm-cloud-sdk-core>=0.4.2 \ No newline at end of file diff --git a/setup.py b/setup.py index 70ba00d..b82440a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages # IbmCos Config sdk python version check -_valid = sys.version_info[:2] == (2, 7) or sys.version_info >= (3,4) +_valid = sys.version_info[:2] == (2, 7) or sys.version_info >= (3,4) if not _valid: sys.exit("Sorry, IBM COS Config SDK only supports versions 2.7, 3.4, 3.5, 3.6, 3.7 of python.") @@ -15,7 +15,7 @@ requirements = [ 'requests', - 'python-dateutil' + 'ibm-cloud-sdk-core>=0.4.2', ] @@ -35,7 +35,7 @@ def get_version(): scripts=[], packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires = requirements, + install_requires=requirements, license="Apache License 2.0", classifiers=[ 'Development Status :: 5 - Production/Stable',