mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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__(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user