diff --git a/intercom/__init__.py b/intercom/__init__.py index 3b099aef..1f65c9eb 100644 --- a/intercom/__init__.py +++ b/intercom/__init__.py @@ -1,21 +1,23 @@ # -*- coding: utf-8 -*- from datetime import datetime -from .errors import ArgumentError -from .errors import HttpError # noqa +from .errors import (ArgumentError, AuthenticationError, # noqa + BadGatewayError, BadRequestError, HttpError, IntercomError, + MultipleMatchingUsersError, RateLimitExceeded, ResourceNotFound, + ServerError, ServiceUnavailableError, UnexpectedError) from .lib.setter_property import SetterProperty from .request import Request -from .admin import Admin -from .company import Company -from .conversation import Conversation -from .event import Event -from .message import Message -from .note import Note -from .notification import Notification -from .user import User -from .segment import Segment -from .subscription import Subscription -from .tag import Tag +from .admin import Admin # noqa +from .company import Company # noqa +from .conversation import Conversation # noqa +from .event import Event # noqa +from .message import Message # noqa +from .note import Note # noqa +from .notification import Notification # noqa +from .user import User # noqa +from .segment import Segment # noqa +from .subscription import Subscription # noqa +from .tag import Tag # noqa import copy import random @@ -24,11 +26,6 @@ __version__ = '2.0.alpha' -__all__ = ( - Admin, Company, Conversation, Event, Message, Note, Notification, - Segment, Subscription, Tag, User -) - RELATED_DOCS_TEXT = "See https://github.com/jkeyes/python-intercom \ for usage examples." diff --git a/intercom/errors.py b/intercom/errors.py index bd6727e3..f08fc432 100644 --- a/intercom/errors.py +++ b/intercom/errors.py @@ -7,3 +7,59 @@ class ArgumentError(ValueError): class HttpError(Exception): pass + + +class IntercomError(Exception): + + def __init__(self, message=None, context=None): + super(IntercomError, self).__init__(message) + self.context = context + + +class ResourceNotFound(IntercomError): + pass + + +class AuthenticationError(IntercomError): + pass + + +class ServerError(IntercomError): + pass + + +class BadGatewayError(IntercomError): + pass + + +class ServiceUnavailableError(IntercomError): + pass + + +class BadRequestError(IntercomError): + pass + + +class RateLimitExceeded(IntercomError): + pass + + +class MultipleMatchingUsersError(IntercomError): + pass + + +class UnexpectedError(IntercomError): + pass + + +error_codes = { + 'unauthorized': AuthenticationError, + 'forbidden': AuthenticationError, + 'bad_request': BadRequestError, + 'missing_parameter': BadRequestError, + 'parameter_invalid': BadRequestError, + 'not_found': ResourceNotFound, + 'rate_limit_exceeded': RateLimitExceeded, + 'service_unavailable': ServiceUnavailableError, + 'conflict': MultipleMatchingUsersError, +} diff --git a/intercom/request.py b/intercom/request.py index 4d01a292..d76ffce8 100644 --- a/intercom/request.py +++ b/intercom/request.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from .errors import HttpError # noqa +from . import errors import json import requests @@ -32,8 +32,71 @@ def send_request_to_path(cls, method, url, auth, params=None): method, url, timeout=cls.timeout, auth=auth, **req_params) + cls.raise_errors_on_failure(resp) + if resp.content: - return json.loads(resp.content) + return cls.parse_body(resp) + + @classmethod + def parse_body(cls, resp): + try: + body = json.loads(resp.content) + except ValueError: + cls.raise_errors_on_failure(resp) + if body.get('type') == 'error.list': + cls.raise_application_errors_on_failure(body, resp.status_code) + return body + + @classmethod + def raise_errors_on_failure(cls, resp): + if resp.status_code == 404: + raise errors.ResourceNotFound('Resource Not Found') + elif resp.status_code == 401: + raise errors.AuthenticationError('Unauthorized') + elif resp.status_code == 403: + raise errors.AuthenticationError('Forbidden') + elif resp.status_code == 500: + raise errors.ServerError('Server Error') + elif resp.status_code == 502: + raise errors.BadGatewayError('Bad Gateway Error') + elif resp.status_code == 503: + raise errors.ServiceUnavailableError('Service Unavailable') + + @classmethod + def raise_application_errors_on_failure(cls, error_list_details, http_code): # noqa + # Currently, we don't support multiple errors + error_details = error_list_details['errors'][0] + error_code = error_details.get('type') + if error_code is None: + error_code = error_details.get('code') + error_context = { + 'http_code': http_code, + 'application_error_code': error_code + } + error_class = errors.error_codes.get(error_code) + if error_class is None: + # unexpected error + if error_code: + message = cls.message_for_unexpected_error_with_type( + error_details, http_code) + else: + message = cls.message_for_unexpected_error_without_type( + error_details, http_code) + error_class = errors.UnexpectedError + else: + message = error_details['message'] + raise error_class(message, error_context) + + @classmethod + def message_for_unexpected_error_with_type(cls, error_details, http_code): # noqa + error_type = error_details['type'] + message = error_details['message'] + 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 + + @classmethod + def message_for_unexpected_error_without_type(cls, error_details, http_code): # noqa + message = error_details['message'] + 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 class ResourceEncoder(json.JSONEncoder): diff --git a/tests/unit/request_spec.py b/tests/unit/request_spec.py new file mode 100644 index 00000000..34017099 --- /dev/null +++ b/tests/unit/request_spec.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- + +import httpretty +import intercom +import json +import re +from describe import expect +from intercom import Intercom +from intercom import UnexpectedError + +get = httpretty.GET +post = httpretty.POST +r = re.compile + + +class DescribeRequest: + + @httpretty.activate + def it_raises_resource_not_found(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=404) + with expect.to_raise_error(intercom.ResourceNotFound): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_authentication_error_unauthorized(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=401) + with expect.to_raise_error(intercom.AuthenticationError): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_authentication_error_forbidden(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=403) + with expect.to_raise_error(intercom.AuthenticationError): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_server_error(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=500) + with expect.to_raise_error(intercom.ServerError): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_bad_gateway_error(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=502) + with expect.to_raise_error(intercom.BadGatewayError): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_service_unavailable_error(self): + httpretty.register_uri( + get, r(r'/notes$'), body='', status=503) + with expect.to_raise_error(intercom.ServiceUnavailableError): + Intercom.get('/notes') + + @httpretty.activate + def it_raises_an_unexpected_typed_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'hopper', + 'message': 'The first compiler.' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + try: + Intercom.get('/users') + except (UnexpectedError) as err: + assert "The error of type 'hopper' is not recognized" in err.message # noqa + expect(err.context['http_code']) == 200 + expect(err.context['application_error_code']) == 'hopper' + + @httpretty.activate + def it_raises_an_unexpected_untyped_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'message': 'UNIVAC' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + try: + Intercom.get('/users') + except (UnexpectedError) as err: + assert "An unexpected error occured." in err.message + expect(err.context['application_error_code']) is None + + @httpretty.activate + def it_raises_a_bad_request_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': None, + 'message': 'email is required' + } + ] + } + + for code in ['missing_parameter', 'parameter_invalid', 'bad_request']: + payload['errors'][0]['type'] = code + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.BadRequestError): + Intercom.get('/users') + + @httpretty.activate + def it_raises_an_authentication_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'unauthorized', + 'message': 'Your name\'s not down.' + } + ] + } + for code in ['unauthorized', 'forbidden']: + payload['errors'][0]['type'] = code + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.AuthenticationError): + Intercom.get('/users') + + @httpretty.activate + def it_raises_resource_not_found_by_type(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'not_found', + 'message': 'Waaaaally?' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.ResourceNotFound): + Intercom.get('/users') + + @httpretty.activate + def it_raises_rate_limit_exceeded(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'rate_limit_exceeded', + 'message': 'Fair use please.' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.RateLimitExceeded): + Intercom.get('/users') + + @httpretty.activate + def it_raises_a_service_unavailable_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'service_unavailable', + 'message': 'Zzzzz.' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.ServiceUnavailableError): + Intercom.get('/users') + + @httpretty.activate + def it_raises_a_multiple_matching_users_error(self): + payload = { + 'type': 'error.list', + 'errors': [ + { + 'type': 'conflict', + 'message': 'Two many cooks.' + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(intercom.MultipleMatchingUsersError): + Intercom.get('/users') diff --git a/tests/unit/user_spec.py b/tests/unit/user_spec.py index 229d10be..b389f2d4 100644 --- a/tests/unit/user_spec.py +++ b/tests/unit/user_spec.py @@ -10,7 +10,9 @@ from describe import expect from intercom.collection_proxy import CollectionProxy from intercom.lib.flat_store import FlatStore +from intercom import Intercom from intercom import User +from intercom import MultipleMatchingUsersError from intercom.utils import create_class_instance from tests.unit import test_user @@ -317,6 +319,21 @@ def it_returns_the_total_number_of_users(self): mock_count.return_value = 100 expect(100) == User.count() + @httpretty.activate + def it_raises_a_multiple_matching_users_error_when_receiving_a_conflict(self): # noqa + payload = { + 'type': 'error.list', + 'errors': [ + { + 'code': 'conflict', + 'message': 'Multiple existing users match this email address - must be more specific using user_id' # noqa + } + ] + } + httpretty.register_uri(get, r("/users"), body=json.dumps(payload)) + with expect.to_raise_error(MultipleMatchingUsersError): + Intercom.get('/users') + class DescribeIncrementingCustomAttributeFields: def before_each(self, context):