Don't require boto3 if Amazon SES webhooks aren't actually used

Delay raising AnymailImproperlyInstalled from webhooks.amazon_ses
until an SES webhook view is instantiated. Allows anymail.urls
to import webhooks.amazon_ses without error.

Fixes #103
This commit is contained in:
medmunds
2018-04-16 15:41:00 -07:00
parent e85c4a911f
commit dd26fd3108
3 changed files with 42 additions and 10 deletions

View File

@@ -177,9 +177,9 @@ class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
"""Exception for Anymail missing package dependencies""" """Exception for Anymail missing package dependencies"""
def __init__(self, missing_package, backend="<backend>"): def __init__(self, missing_package, backend="<backend>"):
message = "The %s package is required to use this backend, but isn't installed.\n" \ message = "The %s package is required to use this ESP, but isn't installed.\n" \
"(Be sure to use `pip install django-anymail[%s]` " \ "(Be sure to use `pip install django-anymail[%s]` " \
"with your desired backends)" % (missing_package, backend) "with your desired ESPs.)" % (missing_package, backend)
super(AnymailImproperlyInstalled, self).__init__(message) super(AnymailImproperlyInstalled, self).__init__(message)
@@ -195,3 +195,17 @@ class AnymailInsecureWebhookWarning(AnymailWarning):
class AnymailDeprecationWarning(AnymailWarning, DeprecationWarning): class AnymailDeprecationWarning(AnymailWarning, DeprecationWarning):
"""Warning for deprecated Anymail features""" """Warning for deprecated Anymail features"""
# Helpers
class _LazyError(object):
"""An object that sits inert unless/until used, then raises an error"""
def __init__(self, error):
self._error = error
def __call__(self, *args, **kwargs):
raise self._error
def __getattr__(self, item):
raise self._error

View File

@@ -6,18 +6,23 @@ from django.http import HttpResponse
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from .base import AnymailBaseWebhookView from .base import AnymailBaseWebhookView
from ..backends.amazon_ses import _get_anymail_boto3_params
from ..exceptions import ( from ..exceptions import (
AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure) AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure,
_LazyError)
from ..inbound import AnymailInboundMessage from ..inbound import AnymailInboundMessage
from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking
from ..utils import combine, get_anymail_setting, getfirst from ..utils import combine, get_anymail_setting, getfirst
try: try:
import boto3 import boto3
import botocore.exceptions from botocore.exceptions import ClientError
from ..backends.amazon_ses import _get_anymail_boto3_params
except ImportError: except ImportError:
raise AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses') # This module gets imported by anymail.urls, so don't complain about boto3 missing
# unless one of the Amazon SES webhook views is actually used and needs it
boto3 = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
ClientError = object
_get_anymail_boto3_params = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
class AmazonSESBaseWebhookView(AnymailBaseWebhookView): class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
@@ -296,7 +301,7 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView):
s3.download_fileobj(bucket_name, object_key, content) s3.download_fileobj(bucket_name, object_key, content)
content.seek(0) content.seek(0)
message = AnymailInboundMessage.parse_raw_mime_file(content) message = AnymailInboundMessage.parse_raw_mime_file(content)
except botocore.exceptions.ClientError as err: except ClientError as err:
# improve the botocore error message # improve the botocore error message
raise AnymailBotoClientAPIError( raise AnymailBotoClientAPIError(
"Anymail AmazonSESInboundWebhookView couldn't download S3 object '{bucket_name}:{object_key}'" "Anymail AmazonSESInboundWebhookView couldn't download S3 object '{bucket_name}:{object_key}'"
@@ -334,11 +339,11 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView):
)] )]
class AnymailBotoClientAPIError(AnymailAPIError, botocore.exceptions.ClientError): class AnymailBotoClientAPIError(AnymailAPIError, ClientError):
"""An AnymailAPIError that is also a Boto ClientError""" """An AnymailAPIError that is also a Boto ClientError"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
raised_from = kwargs.pop('raised_from') raised_from = kwargs.pop('raised_from')
assert isinstance(raised_from, botocore.exceptions.ClientError) assert isinstance(raised_from, ClientError)
assert len(kwargs) == 0 # can't support other kwargs assert len(kwargs) == 0 # can't support other kwargs
# init self as boto ClientError (which doesn't cooperatively subclass): # init self as boto ClientError (which doesn't cooperatively subclass):
super(AnymailBotoClientAPIError, self).__init__( super(AnymailBotoClientAPIError, self).__init__(

View File

@@ -18,7 +18,7 @@ try:
except ImportError: except ImportError:
string_concat = None string_concat = None
from anymail.exceptions import AnymailInvalidAddress from anymail.exceptions import AnymailInvalidAddress, _LazyError
from anymail.utils import ( from anymail.utils import (
parse_address_list, parse_single_address, EmailAddress, parse_address_list, parse_single_address, EmailAddress,
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list, is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
@@ -355,3 +355,16 @@ class ParseRFC2822DateTests(SimpleTestCase):
self.assertIsNone(parse_rfc2822date("Tue, 24 Oct")) self.assertIsNone(parse_rfc2822date("Tue, 24 Oct"))
self.assertIsNone(parse_rfc2822date("Lug, 24 Nod 2017 10:11:35 +0000")) self.assertIsNone(parse_rfc2822date("Lug, 24 Nod 2017 10:11:35 +0000"))
self.assertIsNone(parse_rfc2822date("Tue, 99 Oct 9999 99:99:99 +9999")) self.assertIsNone(parse_rfc2822date("Tue, 99 Oct 9999 99:99:99 +9999"))
class LazyErrorTests(SimpleTestCase):
def test_attr(self):
lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error
lazy.some_prop = "foo" # setattr doesn't cause error
with self.assertRaisesMessage(ValueError, "lazy failure"):
self.unused = lazy.anything # getattr *does* cause error
def test_call(self):
lazy = _LazyError(ValueError("lazy failure")) # creating doesn't cause error
with self.assertRaisesMessage(ValueError, "lazy failure"):
self.unused = lazy() # call *does* cause error