Rename exceptions

* Change Djrill -> Mandrill in exception names
* Don't re-export at package level
  (import from anymail.exceptions, not from anymail)
This commit is contained in:
medmunds
2016-02-27 11:57:53 -08:00
parent 921dd5d0d6
commit 1c7fe8a759
6 changed files with 74 additions and 71 deletions

View File

@@ -1,3 +1 @@
from ._version import __version__, VERSION
from .exceptions import (MandrillAPIError, MandrillRecipientsRefused,
NotSerializableForMandrillError, NotSupportedByMandrillError)

View File

@@ -16,8 +16,8 @@ from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
from .._version import __version__
from ..exceptions import (DjrillError, MandrillAPIError, MandrillRecipientsRefused,
NotSerializableForMandrillError, NotSupportedByMandrillError)
from ..exceptions import (AnymailError, AnymailRequestsAPIError, AnymailRecipientsRefused,
AnymailSerializationError, AnymailUnsupportedFeature)
class MandrillBackend(BaseEmailBackend):
@@ -127,8 +127,8 @@ class MandrillBackend(BaseEmailBackend):
message.mandrill_response = self.parse_response(response, payload, message)
self.validate_response(message.mandrill_response, response, payload, message)
except DjrillError:
# every *expected* error is derived from DjrillError;
except AnymailError:
# every *expected* error is derived from AnymailError;
# we deliberately don't silence unexpected errors
if not self.fail_silently:
raise
@@ -153,7 +153,7 @@ class MandrillBackend(BaseEmailBackend):
payload is a dict that will become the Mandrill send data
message is an EmailMessage, possibly with additional Mandrill-specific attrs
Can raise NotSupportedByMandrillError for unsupported options in message.
Can raise AnymailUnsupportedFeature for unsupported options in message.
"""
msg_dict = self._build_standard_message_dict(message)
self._add_mandrill_options(message, msg_dict)
@@ -192,31 +192,31 @@ class MandrillBackend(BaseEmailBackend):
message is the original EmailMessage
return should be a requests.Response
Can raise NotSerializableForMandrillError if payload is not serializable
Can raise MandrillAPIError for HTTP errors in the post
Can raise AnymailSerializationError if payload is not serializable
Can raise AnymailRequestsAPIError for HTTP errors in the post
"""
api_url = self.get_api_url(payload, message)
try:
json_payload = self.serialize_payload(payload, message)
except TypeError as err:
# Add some context to the "not JSON serializable" message
raise NotSerializableForMandrillError(
raise AnymailSerializationError(
orig_err=err, email_message=message, payload=payload)
response = self.session.post(api_url, data=json_payload)
if response.status_code != 200:
raise MandrillAPIError(email_message=message, payload=payload, response=response)
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
return response
def parse_response(self, response, payload, message):
"""Return parsed json from Mandrill API response
Can raise MandrillAPIError if response is not valid JSON
Can raise AnymailRequestsAPIError if response is not valid JSON
"""
try:
return response.json()
except ValueError:
raise MandrillAPIError("Invalid JSON in Mandrill API response",
raise AnymailRequestsAPIError("Invalid JSON in Mandrill API response",
email_message=message, payload=payload, response=response)
def validate_response(self, parsed_response, response, payload, message):
@@ -233,12 +233,12 @@ class MandrillBackend(BaseEmailBackend):
try:
recipient_status = [item["status"] for item in parsed_response]
except (KeyError, TypeError):
raise MandrillAPIError("Invalid Mandrill API response format",
raise AnymailRequestsAPIError("Invalid Mandrill API response format",
email_message=message, payload=payload, response=response)
# Error if *all* recipients are invalid or refused
# (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
if all([status in ('invalid', 'rejected') for status in recipient_status]):
raise MandrillRecipientsRefused(email_message=message, payload=payload, response=response)
raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response)
#
# Payload construction
@@ -251,7 +251,7 @@ class MandrillBackend(BaseEmailBackend):
use by default. Standard text email messages sent through Django will
still work through Mandrill.
Raises NotSupportedByMandrillError for any standard EmailMessage
Raises AnymailUnsupportedFeature for any standard EmailMessage
features that cannot be accurately communicated to Mandrill.
"""
sender = sanitize_address(message.from_email, message.encoding)
@@ -381,14 +381,14 @@ class MandrillBackend(BaseEmailBackend):
the HTML output for your email.
"""
if len(message.alternatives) > 1:
raise NotSupportedByMandrillError(
raise AnymailUnsupportedFeature(
"Too many alternatives attached to the message. "
"Mandrill only accepts plain text and html emails.",
email_message=message)
(content, mimetype) = message.alternatives[0]
if mimetype != 'text/html':
raise NotSupportedByMandrillError(
raise AnymailUnsupportedFeature(
"Invalid alternative mimetype '%s'. "
"Mandrill only accepts plain text and html emails."
% mimetype,

View File

@@ -2,28 +2,30 @@ import json
from requests import HTTPError
class DjrillError(Exception):
"""Base class for exceptions raised by Djrill
class AnymailError(Exception):
"""Base class for exceptions raised by Anymail
Overrides __str__ to provide additional information about
Mandrill API call and response.
the ESP API call and response.
"""
def __init__(self, *args, **kwargs):
"""
Optional kwargs:
email_message: the original EmailMessage being sent
payload: data arg (*not* json-stringified) for the Mandrill send call
status_code: HTTP status code of response to ESP send call
payload: data arg (*not* json-stringified) for the ESP send call
response: requests.Response from the send call
"""
self.email_message = kwargs.pop('email_message', None)
self.payload = kwargs.pop('payload', None)
self.status_code = kwargs.pop('status_code', None)
if isinstance(self, HTTPError):
# must leave response in kwargs for HTTPError
self.response = kwargs.get('response', None)
else:
self.response = kwargs.pop('response', None)
super(DjrillError, self).__init__(*args, **kwargs)
super(AnymailError, self).__init__(*args, **kwargs)
def __str__(self):
parts = [
@@ -34,7 +36,7 @@ class DjrillError(Exception):
return "\n".join(filter(None, parts))
def describe_send(self):
"""Return a string describing the Mandrill send in self.payload, or None"""
"""Return a string describing the ESP send in self.payload, or None"""
if self.payload is None:
return None
description = "Sending a message"
@@ -50,10 +52,10 @@ class DjrillError(Exception):
return description
def describe_response(self):
"""Return a formatted string of self.response, or None"""
if self.response is None:
"""Return a formatted string of self.status_code and response, or None"""
if self.status_code is None:
return None
description = "Mandrill API response %d:" % self.response.status_code
description = "ESP API response %d:" % self.status_code
try:
json_response = self.response.json()
description += "\n" + json.dumps(json_response, indent=2)
@@ -65,53 +67,56 @@ class DjrillError(Exception):
return description
class MandrillAPIError(DjrillError, HTTPError):
"""Exception for unsuccessful response from Mandrill API."""
class AnymailAPIError(AnymailError):
"""Exception for unsuccessful response from ESP's API."""
class AnymailRequestsAPIError(AnymailAPIError, HTTPError):
"""Exception for unsuccessful response from a requests API."""
def __init__(self, *args, **kwargs):
super(MandrillAPIError, self).__init__(*args, **kwargs)
super(AnymailRequestsAPIError, self).__init__(*args, **kwargs)
if self.response is not None:
self.status_code = self.response.status_code
class MandrillRecipientsRefused(DjrillError):
class AnymailRecipientsRefused(AnymailError):
"""Exception for send where all recipients are invalid or rejected."""
def __init__(self, message=None, *args, **kwargs):
if message is None:
message = "All message recipients were rejected or invalid"
super(MandrillRecipientsRefused, self).__init__(message, *args, **kwargs)
super(AnymailRecipientsRefused, self).__init__(message, *args, **kwargs)
class NotSupportedByMandrillError(DjrillError, ValueError):
"""Exception for email features that Mandrill doesn't support.
class AnymailUnsupportedFeature(AnymailError, ValueError):
"""Exception for Anymail features that the ESP doesn't support.
This is typically raised when attempting to send a Django EmailMessage that
uses options or values you might expect to work, but that are silently
ignored by or can't be communicated to Mandrill's API. (E.g., non-HTML
alternative parts.)
ignored by or can't be communicated to the ESP's API.
It's generally *not* raised for Mandrill-specific features, like limitations
on Mandrill tag names or restrictions on from emails. (Djrill expects
Mandrill to return an API error for these where appropriate, and tries to
avoid duplicating Mandrill's validation logic locally.)
It's generally *not* raised for ESP-specific limitations, like the number
of tags allowed on a message. (Anymail expects
the ESP to return an API error for these where appropriate, and tries to
avoid duplicating each ESP's validation logic locally.)
"""
class NotSerializableForMandrillError(DjrillError, TypeError):
"""Exception for data that Djrill doesn't know how to convert to JSON.
class AnymailSerializationError(AnymailError, TypeError):
"""Exception for data that Anymail can't serialize for the ESP's API.
This typically results from including something like a date or Decimal
in your merge_vars (or other Mandrill-specific EmailMessage option).
in your merge_vars.
"""
# inherits from TypeError for backwards compatibility with Djrill 1.x
# inherits from TypeError for compatibility with JSON serialization error
def __init__(self, message=None, orig_err=None, *args, **kwargs):
if message is None:
message = "Don't know how to send this data to Mandrill. " \
message = "Don't know how to send this data to your ESP. " \
"Try converting it to a string or number first."
if orig_err is not None:
message += "\n%s" % str(orig_err)
super(NotSerializableForMandrillError, self).__init__(message, *args, **kwargs)
super(AnymailSerializationError, self).__init__(message, *args, **kwargs)

View File

@@ -7,7 +7,7 @@ from django.core import mail
from django.test import TestCase
from django.test.utils import override_settings
from anymail import MandrillAPIError, MandrillRecipientsRefused
from anymail.exceptions import AnymailAPIError, AnymailRecipientsRefused
MANDRILL_TEST_API_KEY = os.getenv('MANDRILL_TEST_API_KEY')
@@ -51,7 +51,7 @@ class DjrillIntegrationTests(TestCase):
try:
self.message.send()
self.fail("This line will not be reached, because send() raised an exception")
except MandrillAPIError as err:
except AnymailAPIError as err:
self.assertEqual(err.status_code, 500)
self.assertIn("email address is invalid", str(err))
@@ -60,7 +60,7 @@ class DjrillIntegrationTests(TestCase):
self.message.to = ['invalid@localhost']
try:
self.message.send()
except MandrillRecipientsRefused:
except AnymailRecipientsRefused:
# Mandrill refused to deliver the mail -- message.mandrill_response will tell you why:
# noinspection PyUnresolvedReferences
response = self.message.mandrill_response
@@ -72,14 +72,14 @@ class DjrillIntegrationTests(TestCase):
if response[0]['status'] == 'queued':
self.skipTest("Mandrill queued the send -- can't complete this test")
else:
self.fail("Djrill did not raise MandrillRecipientsRefused for invalid recipient")
self.fail("Djrill did not raise AnymailRecipientsRefused for invalid recipient")
def test_rejected_to(self):
# Example of detecting when a recipient is on Mandrill's rejection blacklist
self.message.to = ['reject@test.mandrillapp.com']
try:
self.message.send()
except MandrillRecipientsRefused:
except AnymailRecipientsRefused:
# Mandrill refused to deliver the mail -- message.mandrill_response will tell you why:
# noinspection PyUnresolvedReferences
response = self.message.mandrill_response
@@ -92,7 +92,7 @@ class DjrillIntegrationTests(TestCase):
if response[0]['status'] == 'queued':
self.skipTest("Mandrill queued the send -- can't complete this test")
else:
self.fail("Djrill did not raise MandrillRecipientsRefused for blacklist recipient")
self.fail("Djrill did not raise AnymailRecipientsRefused for blacklist recipient")
@override_settings(MANDRILL_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):
@@ -100,6 +100,6 @@ class DjrillIntegrationTests(TestCase):
try:
self.message.send()
self.fail("This line will not be reached, because send() raised an exception")
except MandrillAPIError as err:
except AnymailAPIError as err:
self.assertEqual(err.status_code, 500)
self.assertIn("Invalid API key", str(err))

View File

@@ -19,8 +19,8 @@ from django.core.mail import make_msgid
from django.test import TestCase
from django.test.utils import override_settings
from anymail import (MandrillAPIError, MandrillRecipientsRefused,
NotSerializableForMandrillError, NotSupportedByMandrillError)
from anymail.exceptions import (AnymailAPIError, AnymailRecipientsRefused,
AnymailSerializationError, AnymailUnsupportedFeature)
from .mock_backend import DjrillBackendMockAPITestCase
@@ -275,14 +275,14 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
'from@example.com', ['to@example.com'])
email.attach_alternative("<p>First html is OK</p>", "text/html")
email.attach_alternative("<p>But not second html</p>", "text/html")
with self.assertRaises(NotSupportedByMandrillError):
with self.assertRaises(AnymailUnsupportedFeature):
email.send()
# Only html alternatives allowed
email = mail.EmailMultiAlternatives('Subject', 'Body',
'from@example.com', ['to@example.com'])
email.attach_alternative("{'not': 'allowed'}", "application/json")
with self.assertRaises(NotSupportedByMandrillError):
with self.assertRaises(AnymailUnsupportedFeature):
email.send()
# Make sure fail_silently is respected
@@ -296,7 +296,7 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
def test_mandrill_api_failure(self):
self.mock_post.return_value = self.MockResponse(status_code=400)
with self.assertRaises(MandrillAPIError):
with self.assertRaises(AnymailAPIError):
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
['to@example.com'])
self.assertEqual(sent, 0)
@@ -319,17 +319,17 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
"message": "Helpful explanation from Mandrill"
}"""
self.mock_post.return_value = self.MockResponse(status_code=400, raw=error_response)
with self.assertRaisesMessage(MandrillAPIError, "Helpful explanation from Mandrill"):
with self.assertRaisesMessage(AnymailAPIError, "Helpful explanation from Mandrill"):
msg.send()
# Non-JSON error response:
self.mock_post.return_value = self.MockResponse(status_code=500, raw=b"Invalid API key")
with self.assertRaisesMessage(MandrillAPIError, "Invalid API key"):
with self.assertRaisesMessage(AnymailAPIError, "Invalid API key"):
msg.send()
# No content in the error response:
self.mock_post.return_value = self.MockResponse(status_code=502, raw=None)
with self.assertRaises(MandrillAPIError):
with self.assertRaises(AnymailAPIError):
msg.send()
@@ -539,29 +539,29 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
"""If the send succeeds, but a non-JSON API response, should raise an API exception"""
self.mock_post.return_value = self.MockResponse(status_code=500, raw=b"this isn't json")
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
with self.assertRaises(MandrillAPIError):
with self.assertRaises(AnymailAPIError):
msg.send()
self.assertIsNone(msg.mandrill_response)
def test_json_serialization_errors(self):
"""Try to provide more information about non-json-serializable data"""
self.message.global_merge_vars = {'PRICE': Decimal('19.99')}
with self.assertRaises(NotSerializableForMandrillError) as cm:
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
err = cm.exception
self.assertTrue(isinstance(err, TypeError)) # Djrill 1.x re-raised TypeError from json.dumps
self.assertStrContains(str(err), "Don't know how to send this data to Mandrill") # our added context
self.assertStrContains(str(err), "Don't know how to send this data to your ESP") # our added context
self.assertStrContains(str(err), "Decimal('19.99') is not JSON serializable") # original message
def test_dates_not_serialized(self):
"""Pre-2.0 Djrill accidentally serialized dates to ISO"""
self.message.global_merge_vars = {'SHIP_DATE': date(2015, 12, 2)}
with self.assertRaises(NotSerializableForMandrillError):
with self.assertRaises(AnymailSerializationError):
self.message.send()
class DjrillRecipientsRefusedTests(DjrillBackendMockAPITestCase):
"""Djrill raises MandrillRecipientsRefused when *all* recipients are rejected or invalid"""
"""Djrill raises AnymailRecipientsRefused when *all* recipients are rejected or invalid"""
def test_recipients_refused(self):
msg = mail.EmailMessage('Subject', 'Body', 'from@example.com',
@@ -569,7 +569,7 @@ class DjrillRecipientsRefusedTests(DjrillBackendMockAPITestCase):
self.mock_post.return_value = self.MockResponse(status_code=200, raw=b"""
[{ "email": "invalid@localhost", "status": "invalid" },
{ "email": "reject@test.mandrillapp.com", "status": "rejected" }]""")
with self.assertRaises(MandrillRecipientsRefused):
with self.assertRaises(AnymailRecipientsRefused):
msg.send()
def test_fail_silently(self):

View File

@@ -1,6 +1,6 @@
from django.core import mail
from anymail import MandrillAPIError
from anymail.exceptions import AnymailAPIError
from .mock_backend import DjrillBackendMockAPITestCase
@@ -47,7 +47,7 @@ class DjrillMandrillSendTemplateTests(DjrillBackendMockAPITestCase):
'from@example.com', ['to@example.com'])
msg.template_name = "PERSONALIZED_SPECIALS"
msg.use_template_from = True
with self.assertRaises(MandrillAPIError):
with self.assertRaises(AnymailAPIError):
msg.send()
def test_send_template_without_subject_field(self):