Skip to content

Commit a0d9e06

Browse files
committed
Merge pull request #56 from jkeyes/error-handling
Error handling
2 parents 0ee783c + b472fc0 commit a0d9e06

File tree

5 files changed

+342
-20
lines changed

5 files changed

+342
-20
lines changed

intercom/__init__.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
# -*- coding: utf-8 -*-
22

33
from datetime import datetime
4-
from .errors import ArgumentError
5-
from .errors import HttpError # noqa
4+
from .errors import (ArgumentError, AuthenticationError, # noqa
5+
BadGatewayError, BadRequestError, HttpError, IntercomError,
6+
MultipleMatchingUsersError, RateLimitExceeded, ResourceNotFound,
7+
ServerError, ServiceUnavailableError, UnexpectedError)
68
from .lib.setter_property import SetterProperty
79
from .request import Request
8-
from .admin import Admin
9-
from .company import Company
10-
from .conversation import Conversation
11-
from .event import Event
12-
from .message import Message
13-
from .note import Note
14-
from .notification import Notification
15-
from .user import User
16-
from .segment import Segment
17-
from .subscription import Subscription
18-
from .tag import Tag
10+
from .admin import Admin # noqa
11+
from .company import Company # noqa
12+
from .conversation import Conversation # noqa
13+
from .event import Event # noqa
14+
from .message import Message # noqa
15+
from .note import Note # noqa
16+
from .notification import Notification # noqa
17+
from .user import User # noqa
18+
from .segment import Segment # noqa
19+
from .subscription import Subscription # noqa
20+
from .tag import Tag # noqa
1921

2022
import copy
2123
import random
@@ -24,11 +26,6 @@
2426

2527
__version__ = '2.0.alpha'
2628

27-
__all__ = (
28-
Admin, Company, Conversation, Event, Message, Note, Notification,
29-
Segment, Subscription, Tag, User
30-
)
31-
3229

3330
RELATED_DOCS_TEXT = "See https://github.com/jkeyes/python-intercom \
3431
for usage examples."

intercom/errors.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,59 @@ class ArgumentError(ValueError):
77

88
class HttpError(Exception):
99
pass
10+
11+
12+
class IntercomError(Exception):
13+
14+
def __init__(self, message=None, context=None):
15+
super(IntercomError, self).__init__(message)
16+
self.context = context
17+
18+
19+
class ResourceNotFound(IntercomError):
20+
pass
21+
22+
23+
class AuthenticationError(IntercomError):
24+
pass
25+
26+
27+
class ServerError(IntercomError):
28+
pass
29+
30+
31+
class BadGatewayError(IntercomError):
32+
pass
33+
34+
35+
class ServiceUnavailableError(IntercomError):
36+
pass
37+
38+
39+
class BadRequestError(IntercomError):
40+
pass
41+
42+
43+
class RateLimitExceeded(IntercomError):
44+
pass
45+
46+
47+
class MultipleMatchingUsersError(IntercomError):
48+
pass
49+
50+
51+
class UnexpectedError(IntercomError):
52+
pass
53+
54+
55+
error_codes = {
56+
'unauthorized': AuthenticationError,
57+
'forbidden': AuthenticationError,
58+
'bad_request': BadRequestError,
59+
'missing_parameter': BadRequestError,
60+
'parameter_invalid': BadRequestError,
61+
'not_found': ResourceNotFound,
62+
'rate_limit_exceeded': RateLimitExceeded,
63+
'service_unavailable': ServiceUnavailableError,
64+
'conflict': MultipleMatchingUsersError,
65+
}

intercom/request.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
from .errors import HttpError # noqa
3+
from . import errors
44

55
import json
66
import requests
@@ -32,8 +32,71 @@ def send_request_to_path(cls, method, url, auth, params=None):
3232
method, url, timeout=cls.timeout,
3333
auth=auth, **req_params)
3434

35+
cls.raise_errors_on_failure(resp)
36+
3537
if resp.content:
36-
return json.loads(resp.content)
38+
return cls.parse_body(resp)
39+
40+
@classmethod
41+
def parse_body(cls, resp):
42+
try:
43+
body = json.loads(resp.content)
44+
except ValueError:
45+
cls.raise_errors_on_failure(resp)
46+
if body.get('type') == 'error.list':
47+
cls.raise_application_errors_on_failure(body, resp.status_code)
48+
return body
49+
50+
@classmethod
51+
def raise_errors_on_failure(cls, resp):
52+
if resp.status_code == 404:
53+
raise errors.ResourceNotFound('Resource Not Found')
54+
elif resp.status_code == 401:
55+
raise errors.AuthenticationError('Unauthorized')
56+
elif resp.status_code == 403:
57+
raise errors.AuthenticationError('Forbidden')
58+
elif resp.status_code == 500:
59+
raise errors.ServerError('Server Error')
60+
elif resp.status_code == 502:
61+
raise errors.BadGatewayError('Bad Gateway Error')
62+
elif resp.status_code == 503:
63+
raise errors.ServiceUnavailableError('Service Unavailable')
64+
65+
@classmethod
66+
def raise_application_errors_on_failure(cls, error_list_details, http_code): # noqa
67+
# Currently, we don't support multiple errors
68+
error_details = error_list_details['errors'][0]
69+
error_code = error_details.get('type')
70+
if error_code is None:
71+
error_code = error_details.get('code')
72+
error_context = {
73+
'http_code': http_code,
74+
'application_error_code': error_code
75+
}
76+
error_class = errors.error_codes.get(error_code)
77+
if error_class is None:
78+
# unexpected error
79+
if error_code:
80+
message = cls.message_for_unexpected_error_with_type(
81+
error_details, http_code)
82+
else:
83+
message = cls.message_for_unexpected_error_without_type(
84+
error_details, http_code)
85+
error_class = errors.UnexpectedError
86+
else:
87+
message = error_details['message']
88+
raise error_class(message, error_context)
89+
90+
@classmethod
91+
def message_for_unexpected_error_with_type(cls, error_details, http_code): # noqa
92+
error_type = error_details['type']
93+
message = error_details['message']
94+
return "The error of type '%s' is not recognized. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (error_type, message, http_code) # noqa
95+
96+
@classmethod
97+
def message_for_unexpected_error_without_type(cls, error_details, http_code): # noqa
98+
message = error_details['message']
99+
return "An unexpected error occured. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (message, http_code) # noqa
37100

38101

39102
class ResourceEncoder(json.JSONEncoder):

tests/unit/request_spec.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import httpretty
4+
import intercom
5+
import json
6+
import re
7+
from describe import expect
8+
from intercom import Intercom
9+
from intercom import UnexpectedError
10+
11+
get = httpretty.GET
12+
post = httpretty.POST
13+
r = re.compile
14+
15+
16+
class DescribeRequest:
17+
18+
@httpretty.activate
19+
def it_raises_resource_not_found(self):
20+
httpretty.register_uri(
21+
get, r(r'/notes$'), body='', status=404)
22+
with expect.to_raise_error(intercom.ResourceNotFound):
23+
Intercom.get('/notes')
24+
25+
@httpretty.activate
26+
def it_raises_authentication_error_unauthorized(self):
27+
httpretty.register_uri(
28+
get, r(r'/notes$'), body='', status=401)
29+
with expect.to_raise_error(intercom.AuthenticationError):
30+
Intercom.get('/notes')
31+
32+
@httpretty.activate
33+
def it_raises_authentication_error_forbidden(self):
34+
httpretty.register_uri(
35+
get, r(r'/notes$'), body='', status=403)
36+
with expect.to_raise_error(intercom.AuthenticationError):
37+
Intercom.get('/notes')
38+
39+
@httpretty.activate
40+
def it_raises_server_error(self):
41+
httpretty.register_uri(
42+
get, r(r'/notes$'), body='', status=500)
43+
with expect.to_raise_error(intercom.ServerError):
44+
Intercom.get('/notes')
45+
46+
@httpretty.activate
47+
def it_raises_bad_gateway_error(self):
48+
httpretty.register_uri(
49+
get, r(r'/notes$'), body='', status=502)
50+
with expect.to_raise_error(intercom.BadGatewayError):
51+
Intercom.get('/notes')
52+
53+
@httpretty.activate
54+
def it_raises_service_unavailable_error(self):
55+
httpretty.register_uri(
56+
get, r(r'/notes$'), body='', status=503)
57+
with expect.to_raise_error(intercom.ServiceUnavailableError):
58+
Intercom.get('/notes')
59+
60+
@httpretty.activate
61+
def it_raises_an_unexpected_typed_error(self):
62+
payload = {
63+
'type': 'error.list',
64+
'errors': [
65+
{
66+
'type': 'hopper',
67+
'message': 'The first compiler.'
68+
}
69+
]
70+
}
71+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
72+
try:
73+
Intercom.get('/users')
74+
except (UnexpectedError) as err:
75+
assert "The error of type 'hopper' is not recognized" in err.message # noqa
76+
expect(err.context['http_code']) == 200
77+
expect(err.context['application_error_code']) == 'hopper'
78+
79+
@httpretty.activate
80+
def it_raises_an_unexpected_untyped_error(self):
81+
payload = {
82+
'type': 'error.list',
83+
'errors': [
84+
{
85+
'message': 'UNIVAC'
86+
}
87+
]
88+
}
89+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
90+
try:
91+
Intercom.get('/users')
92+
except (UnexpectedError) as err:
93+
assert "An unexpected error occured." in err.message
94+
expect(err.context['application_error_code']) is None
95+
96+
@httpretty.activate
97+
def it_raises_a_bad_request_error(self):
98+
payload = {
99+
'type': 'error.list',
100+
'errors': [
101+
{
102+
'type': None,
103+
'message': 'email is required'
104+
}
105+
]
106+
}
107+
108+
for code in ['missing_parameter', 'parameter_invalid', 'bad_request']:
109+
payload['errors'][0]['type'] = code
110+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
111+
with expect.to_raise_error(intercom.BadRequestError):
112+
Intercom.get('/users')
113+
114+
@httpretty.activate
115+
def it_raises_an_authentication_error(self):
116+
payload = {
117+
'type': 'error.list',
118+
'errors': [
119+
{
120+
'type': 'unauthorized',
121+
'message': 'Your name\'s not down.'
122+
}
123+
]
124+
}
125+
for code in ['unauthorized', 'forbidden']:
126+
payload['errors'][0]['type'] = code
127+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
128+
with expect.to_raise_error(intercom.AuthenticationError):
129+
Intercom.get('/users')
130+
131+
@httpretty.activate
132+
def it_raises_resource_not_found_by_type(self):
133+
payload = {
134+
'type': 'error.list',
135+
'errors': [
136+
{
137+
'type': 'not_found',
138+
'message': 'Waaaaally?'
139+
}
140+
]
141+
}
142+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
143+
with expect.to_raise_error(intercom.ResourceNotFound):
144+
Intercom.get('/users')
145+
146+
@httpretty.activate
147+
def it_raises_rate_limit_exceeded(self):
148+
payload = {
149+
'type': 'error.list',
150+
'errors': [
151+
{
152+
'type': 'rate_limit_exceeded',
153+
'message': 'Fair use please.'
154+
}
155+
]
156+
}
157+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
158+
with expect.to_raise_error(intercom.RateLimitExceeded):
159+
Intercom.get('/users')
160+
161+
@httpretty.activate
162+
def it_raises_a_service_unavailable_error(self):
163+
payload = {
164+
'type': 'error.list',
165+
'errors': [
166+
{
167+
'type': 'service_unavailable',
168+
'message': 'Zzzzz.'
169+
}
170+
]
171+
}
172+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
173+
with expect.to_raise_error(intercom.ServiceUnavailableError):
174+
Intercom.get('/users')
175+
176+
@httpretty.activate
177+
def it_raises_a_multiple_matching_users_error(self):
178+
payload = {
179+
'type': 'error.list',
180+
'errors': [
181+
{
182+
'type': 'conflict',
183+
'message': 'Two many cooks.'
184+
}
185+
]
186+
}
187+
httpretty.register_uri(get, r("/users"), body=json.dumps(payload))
188+
with expect.to_raise_error(intercom.MultipleMatchingUsersError):
189+
Intercom.get('/users')

0 commit comments

Comments
 (0)