Skip to content

Commit f70ec85

Browse files
author
Mike Kistler
committed
Create custom exception for API errors containing the status code
1 parent 2acf477 commit f70ec85

8 files changed

Lines changed: 108 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: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ def test_create_counterexample():
5656
def test_rate_limit_exceeded():
5757
endpoint = '/v1/workspaces/{0}/counterexamples'.format('boguswid')
5858
url = '{0}{1}'.format(base_url, endpoint)
59-
error_code = "'code': '407'"
59+
error_code = 429
6060
error_msg = 'Rate limit exceeded'
6161
responses.add(
6262
responses.POST,
6363
url,
6464
body='Rate limit exceeded',
65-
status=407,
65+
status=429,
6666
content_type='application/json')
6767
service = watson_developer_cloud.ConversationV1(
6868
username='username', password='password', version='2017-02-03')
@@ -71,7 +71,8 @@ def test_rate_limit_exceeded():
7171
workspace_id='boguswid', text='I want financial advice today.')
7272
except WatsonException as ex:
7373
assert len(responses.calls) == 1
74-
assert error_code in str(ex)
74+
assert isinstance(ex, WatsonApiException)
75+
assert error_code == ex.code
7576
assert error_msg in str(ex)
7677

7778
@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: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,30 @@ 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.code = code
66+
self.info = info
67+
68+
def __str__(self):
69+
return 'Error: ' + self.message + ', Code: ' + str(self.code)
70+
71+
5172
class WatsonInvalidArgument(WatsonException):
5273
pass
5374

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

116137
if api_key is not None:
117138
if username is not None or password is not None:
118-
raise WatsonInvalidArgument(
139+
raise ValueError(
119140
'Cannot set api_key and username and password together')
120141
self.set_api_key(api_key)
121142
else:
@@ -138,10 +159,9 @@ def __init__(self, vcap_services_name, url, username=None, password=None,
138159

139160
if (self.username is None or self.password is None)\
140161
and self.api_key is None:
141-
raise WatsonException(
162+
raise ValueError(
142163
'You must specify your username and password service '
143-
'credentials ' +
144-
'(Note: these are different from your Bluemix id)')
164+
'credentials (Note: these are different from your Bluemix id)')
145165

146166
def set_username_and_password(self, username=None, password=None):
147167
if username == 'YOUR SERVICE USERNAME':
@@ -171,7 +191,7 @@ def set_default_headers(self, headers):
171191
if isinstance(headers, dict):
172192
self.default_headers = headers
173193
else:
174-
raise WatsonException("headers parameter must be a dictionary")
194+
raise TypeError("headers parameter must be a dictionary")
175195

176196
# Could make this compute the label_id based on the variable name of the
177197
# dictionary passed in (using **kwargs), but
@@ -192,33 +212,45 @@ def _convert_model(val):
192212
def _get_error_message(response):
193213
"""
194214
Gets the error message from a JSON response.
195-
{
196-
code: 400
197-
error: 'Bad request'
198-
}
215+
:return: the error message
216+
:rtype: string
199217
"""
200218
error_message = 'Unknown error'
201219
try:
202220
error_json = response.json()
203221
if 'error' in error_json:
204222
if isinstance(error_json['error'], dict) and 'description' in \
205223
error_json['error']:
206-
error_message = 'Error: ' + error_json['error'][
207-
'description']
224+
error_message = error_json['error']['description']
208225
else:
209-
error_message = 'Error: ' + error_json['error']
226+
error_message = error_json['error']
210227
elif 'error_message' in error_json:
211-
error_message = 'Error: ' + error_json['error_message']
228+
error_message = error_json['error_message']
212229
elif 'msg' in error_json:
213-
error_message = 'Error: ' + error_json['msg']
230+
error_message = error_json['msg']
214231
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)
232+
error_message = error_json['statusInfo']
219233
return error_message
220234
except:
221-
return {'error': response.text or error_message, 'code': str(response.status_code)}
235+
return (response.text or error_message)
236+
237+
238+
@staticmethod
239+
def _get_error_info(response):
240+
"""
241+
Gets the error info (if any) from a JSON response.
242+
:return: A `dict` containing additional information about the error.
243+
:rtype: dict
244+
"""
245+
info_keys = ['code_description', 'description', 'errors', 'help',
246+
'sub_code', 'warnings']
247+
error_info = {}
248+
try:
249+
error_json = response.json()
250+
error_info = {k:v for k, v in error_json.items() if k in info_keys}
251+
except:
252+
pass
253+
return error_info if any(error_info) else None
222254

223255

224256
def _alchemy_html_request(self, method_name=None, url=None, html=None,
@@ -333,13 +365,14 @@ def request(self, method, url, accept_json=False, headers=None,
333365
response_json = response.json()
334366
if 'status' in response_json and response_json['status'] \
335367
== 'ERROR':
336-
response.status_code = 400
368+
status_code = 400
337369
error_message = 'Unknown error'
370+
338371
if 'statusInfo' in response_json:
339372
error_message = response_json['statusInfo']
340373
if error_message == 'invalid-api-key':
341-
response.status_code = 401
342-
raise WatsonException('Error: ' + error_message)
374+
status_code = 401
375+
raise WatsonApiException(status_code, error_message)
343376
return response_json
344377
return response
345378
else:
@@ -348,4 +381,6 @@ def request(self, method, url, accept_json=False, headers=None,
348381
'invalid credentials '
349382
else:
350383
error_message = self._get_error_message(response)
351-
raise WatsonException(error_message)
384+
error_info = self._get_error_info(response)
385+
raise WatsonApiException(response.status_code, error_message,
386+
error_info)

0 commit comments

Comments
 (0)