Skip to content

Commit d8bc4ea

Browse files
Merge pull request #297 from watson-developer-cloud/mdk/279-improve-exceptions
Create custom exception for API errors containing the status code
2 parents 2acf477 + 70cd9d2 commit d8bc4ea

8 files changed

Lines changed: 110 additions & 34 deletions

File tree

examples/conversation_tone_analyzer_integration/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from .watson_service import WatsonService
1616
from .watson_service import WatsonException
17-
from .watson_service import WatsonInvalidArgument
1817
from .conversation_v1 import ConversationV1
1918
from .tone_analyzer_v3 import ToneAnalyzerV3
2019

examples/natural_language_classifier_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
# delete = natural_language_classifier.remove('2374f9x68-nlc-2697')
3434
# print(json.dumps(delete, indent=2))
3535

36-
# example of raising a WatsonException
36+
# example of raising a ValueError
3737
# print(json.dumps(
3838
# natural_language_classifier.create(training_data='', name='weather3'),
3939
# indent=2))

test/test_conversation_v1.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import responses
1717
import watson_developer_cloud
1818
from watson_developer_cloud import WatsonException
19+
from watson_developer_cloud import WatsonApiException
1920
from watson_developer_cloud.conversation_v1 import *
2021

2122
platform_url = 'https://gateway.watsonplatform.net'
@@ -56,13 +57,13 @@ def test_create_counterexample():
5657
def test_rate_limit_exceeded():
5758
endpoint = '/v1/workspaces/{0}/counterexamples'.format('boguswid')
5859
url = '{0}{1}'.format(base_url, endpoint)
59-
error_code = "'code': '407'"
60+
error_code = 429
6061
error_msg = 'Rate limit exceeded'
6162
responses.add(
6263
responses.POST,
6364
url,
6465
body='Rate limit exceeded',
65-
status=407,
66+
status=429,
6667
content_type='application/json')
6768
service = watson_developer_cloud.ConversationV1(
6869
username='username', password='password', version='2017-02-03')
@@ -71,7 +72,8 @@ def test_rate_limit_exceeded():
7172
workspace_id='boguswid', text='I want financial advice today.')
7273
except WatsonException as ex:
7374
assert len(responses.calls) == 1
74-
assert error_code in str(ex)
75+
assert isinstance(ex, WatsonApiException)
76+
assert error_code == ex.code
7577
assert error_msg in str(ex)
7678

7779
@responses.activate

test/test_natural_language_understanding.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ def test_version_date(self):
6868
@pytest.mark.skipif(os.getenv('VCAP_SERVICES') is not None,
6969
reason='credentials may come from VCAP_SERVICES')
7070
def test_missing_credentials(self):
71-
with pytest.raises(WatsonException):
71+
with pytest.raises(ValueError):
7272
NaturalLanguageUnderstandingV1(version='2016-01-23')
73-
with pytest.raises(WatsonException):
73+
with pytest.raises(ValueError):
7474
NaturalLanguageUnderstandingV1(version='2016-01-23',
7575
url='https://bogus.com')
7676

test/test_tone_analyzer_v3.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import responses
22
import watson_developer_cloud
3+
from watson_developer_cloud import WatsonException
4+
from watson_developer_cloud import WatsonApiException
35
import os
6+
import json
47

58

69
@responses.activate
@@ -104,3 +107,40 @@ def test_tone_chat():
104107
assert responses.calls[0].request.url == tone_url + tone_args
105108
assert responses.calls[0].response.text == tone_response
106109
assert len(responses.calls) == 1
110+
111+
112+
#########################
113+
# error response
114+
#########################
115+
116+
117+
@responses.activate
118+
def test_error():
119+
tone_url = 'https://gateway.watsonplatform.net/tone-analyzer/api/v3/tone'
120+
tone_args = '?version=2016-05-19'
121+
error_code = 400
122+
error_message = "Invalid JSON input at line 2, column 12"
123+
tone_response = {
124+
"code": error_code,
125+
"sub_code": "C00012",
126+
"error": error_message
127+
}
128+
responses.add(responses.POST,
129+
tone_url,
130+
body=json.dumps(tone_response),
131+
status=error_code,
132+
content_type='application/json')
133+
134+
tone_analyzer = watson_developer_cloud.ToneAnalyzerV3("2016-05-19",
135+
username="username", password="password")
136+
text = "Team, I know that times are tough!"
137+
try:
138+
tone_analyzer.tone(text)
139+
except WatsonException as ex:
140+
assert len(responses.calls) == 1
141+
assert isinstance(ex, WatsonApiException)
142+
assert ex.code == error_code
143+
assert ex.message == error_message
144+
assert len(ex.info) == 1
145+
assert ex.info['sub_code'] == 'C00012'
146+

watson_developer_cloud/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from .watson_service import WatsonService
1616
from .watson_service import WatsonException
17+
from .watson_service import WatsonApiException
1718
from .watson_service import WatsonInvalidArgument
1819
from .alchemy_data_news_v1 import AlchemyDataNewsV1
1920
from .alchemy_language_v1 import AlchemyLanguageV1

watson_developer_cloud/language_translation_v2.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
from __future__ import print_function
2121
from .watson_service import WatsonService
22-
from .watson_service import WatsonInvalidArgument
2322

2423

2524
class LanguageTranslationV2(WatsonService):
@@ -56,8 +55,7 @@ def create_model(self, base_model_id, name=None, forced_glossary=None,
5655
monolingual_corpus=None):
5756
if forced_glossary is None and parallel_corpus is None and \
5857
monolingual_corpus is None:
59-
raise WatsonInvalidArgument(
60-
'A glossary or corpus must be provided')
58+
raise ValueError('A glossary or corpus must be provided')
6159
params = {'name': name,
6260
'base_model_id': base_model_id}
6361
files = {'forced_glossary': forced_glossary,
@@ -80,7 +78,7 @@ def translate(self, text, source=None, target=None, model_id=None):
8078
Translates text from a source language to a target language
8179
"""
8280
if model_id is None and (source is None or target is None):
83-
raise WatsonInvalidArgument(
81+
raise ValueError(
8482
'Either model_id or source and target must be specified')
8583

8684
data = {'text': text, 'source': source, 'target': target,

watson_developer_cloud/watson_service.py

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,31 @@ def load_from_vcap_services(service_name):
4545

4646

4747
class WatsonException(Exception):
48+
"""
49+
Custom exception class for Watson Services.
50+
"""
4851
pass
4952

5053

54+
class WatsonApiException(WatsonException):
55+
"""
56+
Custom exception class for errors returned from Watson APIs.
57+
58+
:param int code: The HTTP status code returned.
59+
:param str message: A message describing the error.
60+
:param dict info: A dictionary of additional information about the error.
61+
"""
62+
def __init__(self, code, message, info=None):
63+
# Call the base class constructor with the parameters it needs
64+
super(WatsonApiException, self).__init__(message)
65+
self.message = message
66+
self.code = code
67+
self.info = info
68+
69+
def __str__(self):
70+
return 'Error: ' + self.message + ', Code: ' + str(self.code)
71+
72+
5173
class WatsonInvalidArgument(WatsonException):
5274
pass
5375

@@ -115,7 +137,7 @@ def __init__(self, vcap_services_name, url, username=None, password=None,
115137

116138
if api_key is not None:
117139
if username is not None or password is not None:
118-
raise WatsonInvalidArgument(
140+
raise ValueError(
119141
'Cannot set api_key and username and password together')
120142
self.set_api_key(api_key)
121143
else:
@@ -138,10 +160,9 @@ def __init__(self, vcap_services_name, url, username=None, password=None,
138160

139161
if (self.username is None or self.password is None)\
140162
and self.api_key is None:
141-
raise WatsonException(
163+
raise ValueError(
142164
'You must specify your username and password service '
143-
'credentials ' +
144-
'(Note: these are different from your Bluemix id)')
165+
'credentials (Note: these are different from your Bluemix id)')
145166

146167
def set_username_and_password(self, username=None, password=None):
147168
if username == 'YOUR SERVICE USERNAME':
@@ -171,7 +192,7 @@ def set_default_headers(self, headers):
171192
if isinstance(headers, dict):
172193
self.default_headers = headers
173194
else:
174-
raise WatsonException("headers parameter must be a dictionary")
195+
raise TypeError("headers parameter must be a dictionary")
175196

176197
# Could make this compute the label_id based on the variable name of the
177198
# dictionary passed in (using **kwargs), but
@@ -192,33 +213,45 @@ def _convert_model(val):
192213
def _get_error_message(response):
193214
"""
194215
Gets the error message from a JSON response.
195-
{
196-
code: 400
197-
error: 'Bad request'
198-
}
216+
:return: the error message
217+
:rtype: string
199218
"""
200219
error_message = 'Unknown error'
201220
try:
202221
error_json = response.json()
203222
if 'error' in error_json:
204223
if isinstance(error_json['error'], dict) and 'description' in \
205224
error_json['error']:
206-
error_message = 'Error: ' + error_json['error'][
207-
'description']
225+
error_message = error_json['error']['description']
208226
else:
209-
error_message = 'Error: ' + error_json['error']
227+
error_message = error_json['error']
210228
elif 'error_message' in error_json:
211-
error_message = 'Error: ' + error_json['error_message']
229+
error_message = error_json['error_message']
212230
elif 'msg' in error_json:
213-
error_message = 'Error: ' + error_json['msg']
231+
error_message = error_json['msg']
214232
elif 'statusInfo' in error_json:
215-
error_message = 'Error: ' + error_json['statusInfo']
216-
if 'description' in error_json:
217-
error_message += ', Description: ' + error_json['description']
218-
error_message += ', Code: ' + str(response.status_code)
233+
error_message = error_json['statusInfo']
219234
return error_message
220235
except:
221-
return {'error': response.text or error_message, 'code': str(response.status_code)}
236+
return response.text or error_message
237+
238+
239+
@staticmethod
240+
def _get_error_info(response):
241+
"""
242+
Gets the error info (if any) from a JSON response.
243+
:return: A `dict` containing additional information about the error.
244+
:rtype: dict
245+
"""
246+
info_keys = ['code_description', 'description', 'errors', 'help',
247+
'sub_code', 'warnings']
248+
error_info = {}
249+
try:
250+
error_json = response.json()
251+
error_info = {k:v for k, v in error_json.items() if k in info_keys}
252+
except:
253+
pass
254+
return error_info if any(error_info) else None
222255

223256

224257
def _alchemy_html_request(self, method_name=None, url=None, html=None,
@@ -333,13 +366,14 @@ def request(self, method, url, accept_json=False, headers=None,
333366
response_json = response.json()
334367
if 'status' in response_json and response_json['status'] \
335368
== 'ERROR':
336-
response.status_code = 400
369+
status_code = 400
337370
error_message = 'Unknown error'
371+
338372
if 'statusInfo' in response_json:
339373
error_message = response_json['statusInfo']
340374
if error_message == 'invalid-api-key':
341-
response.status_code = 401
342-
raise WatsonException('Error: ' + error_message)
375+
status_code = 401
376+
raise WatsonApiException(status_code, error_message)
343377
return response_json
344378
return response
345379
else:
@@ -348,4 +382,6 @@ def request(self, method, url, accept_json=False, headers=None,
348382
'invalid credentials '
349383
else:
350384
error_message = self._get_error_message(response)
351-
raise WatsonException(error_message)
385+
error_info = self._get_error_info(response)
386+
raise WatsonApiException(response.status_code, error_message,
387+
error_info)

0 commit comments

Comments
 (0)