mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Make requests optional for backends that don't need it
(Prep for installing backends as package extras) * Extract AnymailRequestsBackend and RequestsPayload to base_requests.py * Don't define/require requests exceptions when requests not available
This commit is contained in:
@@ -1,15 +1,8 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
from six.moves.urllib.parse import urljoin
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail.backends.base import BaseEmailBackend
|
from django.core.mail.backends.base import BaseEmailBackend
|
||||||
|
|
||||||
from ..exceptions import AnymailError, AnymailRequestsAPIError, AnymailSerializationError, AnymailUnsupportedFeature
|
from ..exceptions import AnymailError, AnymailUnsupportedFeature
|
||||||
from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting
|
from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting
|
||||||
from .._version import __version__
|
|
||||||
|
|
||||||
|
|
||||||
class AnymailBaseBackend(BaseEmailBackend):
|
class AnymailBaseBackend(BaseEmailBackend):
|
||||||
@@ -180,78 +173,6 @@ class AnymailBaseBackend(BaseEmailBackend):
|
|||||||
return self.__class__.__name__.replace("Backend", "")
|
return self.__class__.__name__.replace("Backend", "")
|
||||||
|
|
||||||
|
|
||||||
class AnymailRequestsBackend(AnymailBaseBackend):
|
|
||||||
"""
|
|
||||||
Base Anymail email backend for ESPs that use an HTTP API via requests
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, api_url, **kwargs):
|
|
||||||
"""Init options from Django settings"""
|
|
||||||
self.api_url = api_url
|
|
||||||
super(AnymailRequestsBackend, self).__init__(**kwargs)
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
if self.session:
|
|
||||||
return False # already exists
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.session = requests.Session()
|
|
||||||
except requests.RequestException:
|
|
||||||
if not self.fail_silently:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
self.session.headers["User-Agent"] = "Anymail/%s %s" % (
|
|
||||||
__version__, self.session.headers.get("User-Agent", ""))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.session is None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.session.close()
|
|
||||||
except requests.RequestException:
|
|
||||||
if not self.fail_silently:
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def _send(self, message):
|
|
||||||
if self.session is None:
|
|
||||||
class_name = self.__class__.__name__
|
|
||||||
raise RuntimeError(
|
|
||||||
"Session has not been opened in {class_name}._send. "
|
|
||||||
"(This is either an implementation error in {class_name}, "
|
|
||||||
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
|
||||||
return super(AnymailRequestsBackend, self)._send(message)
|
|
||||||
|
|
||||||
def post_to_esp(self, payload, message):
|
|
||||||
"""Post payload to ESP send API endpoint, and return the raw response.
|
|
||||||
|
|
||||||
payload is the result of build_message_payload
|
|
||||||
message is the original EmailMessage
|
|
||||||
return should be a requests.Response
|
|
||||||
|
|
||||||
Can raise AnymailRequestsAPIError for HTTP errors in the post
|
|
||||||
"""
|
|
||||||
params = payload.get_request_params(self.api_url)
|
|
||||||
response = self.session.request(**params)
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def deserialize_response(self, response, payload, message):
|
|
||||||
"""Return parsed ESP API response
|
|
||||||
|
|
||||||
Can raise AnymailRequestsAPIError if response is unparsable
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return response.json()
|
|
||||||
except ValueError:
|
|
||||||
raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
|
|
||||||
email_message=message, payload=payload, response=response)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePayload(object):
|
class BasePayload(object):
|
||||||
# attr, combiner, converter
|
# attr, combiner, converter
|
||||||
base_message_attrs = (
|
base_message_attrs = (
|
||||||
@@ -407,62 +328,3 @@ class BasePayload(object):
|
|||||||
# ESP-specific payload construction
|
# ESP-specific payload construction
|
||||||
def set_esp_extra(self, extra):
|
def set_esp_extra(self, extra):
|
||||||
self.unsupported_feature("esp_extra")
|
self.unsupported_feature("esp_extra")
|
||||||
|
|
||||||
|
|
||||||
class RequestsPayload(BasePayload):
|
|
||||||
"""Abstract Payload for AnymailRequestsBackend"""
|
|
||||||
|
|
||||||
def __init__(self, message, defaults, backend,
|
|
||||||
method="POST", params=None, data=None,
|
|
||||||
headers=None, files=None, auth=None):
|
|
||||||
self.method = method
|
|
||||||
self.params = params
|
|
||||||
self.data = data
|
|
||||||
self.headers = headers
|
|
||||||
self.files = files
|
|
||||||
self.auth = auth
|
|
||||||
super(RequestsPayload, self).__init__(message, defaults, backend)
|
|
||||||
|
|
||||||
def get_request_params(self, api_url):
|
|
||||||
"""Returns a dict of requests.request params that will send payload to the ESP.
|
|
||||||
|
|
||||||
:param api_url: the base api_url for the backend
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
api_endpoint = self.get_api_endpoint()
|
|
||||||
if api_endpoint is not None:
|
|
||||||
url = urljoin(api_url, api_endpoint)
|
|
||||||
else:
|
|
||||||
url = api_url
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
method=self.method,
|
|
||||||
url=url,
|
|
||||||
params=self.params,
|
|
||||||
data=self.serialize_data(),
|
|
||||||
headers=self.headers,
|
|
||||||
files=self.files,
|
|
||||||
auth=self.auth,
|
|
||||||
# json= is not here, because we prefer to do our own serialization
|
|
||||||
# to provide extra context in error messages
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_api_endpoint(self):
|
|
||||||
"""Returns a str that should be joined to the backend's api_url for sending this payload."""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def serialize_data(self):
|
|
||||||
"""Performs any necessary serialization on self.data, and returns the result."""
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
def serialize_json(self, data):
|
|
||||||
"""Returns data serialized to json, raising appropriate errors.
|
|
||||||
|
|
||||||
Useful for implementing serialize_data in a subclass,
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return json.dumps(data)
|
|
||||||
except TypeError as err:
|
|
||||||
# Add some context to the "not JSON serializable" message
|
|
||||||
raise AnymailSerializationError(orig_err=err, email_message=self.message,
|
|
||||||
backend=self.backend, payload=self)
|
|
||||||
|
|||||||
145
anymail/backends/base_requests.py
Normal file
145
anymail/backends/base_requests.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
from six.moves.urllib.parse import urljoin
|
||||||
|
|
||||||
|
from .base import AnymailBaseBackend, BasePayload
|
||||||
|
from ..exceptions import AnymailImproperlyInstalled, AnymailRequestsAPIError, AnymailSerializationError
|
||||||
|
from .._version import __version__
|
||||||
|
|
||||||
|
try:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
raise AnymailImproperlyInstalled('requests')
|
||||||
|
|
||||||
|
|
||||||
|
class AnymailRequestsBackend(AnymailBaseBackend):
|
||||||
|
"""
|
||||||
|
Base Anymail email backend for ESPs that use an HTTP API via requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_url, **kwargs):
|
||||||
|
"""Init options from Django settings"""
|
||||||
|
self.api_url = api_url
|
||||||
|
super(AnymailRequestsBackend, self).__init__(**kwargs)
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
if self.session:
|
||||||
|
return False # already exists
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session = requests.Session()
|
||||||
|
except requests.RequestException:
|
||||||
|
if not self.fail_silently:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.session.headers["User-Agent"] = "Anymail/%s %s" % (
|
||||||
|
__version__, self.session.headers.get("User-Agent", ""))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.session is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.session.close()
|
||||||
|
except requests.RequestException:
|
||||||
|
if not self.fail_silently:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def _send(self, message):
|
||||||
|
if self.session is None:
|
||||||
|
class_name = self.__class__.__name__
|
||||||
|
raise RuntimeError(
|
||||||
|
"Session has not been opened in {class_name}._send. "
|
||||||
|
"(This is either an implementation error in {class_name}, "
|
||||||
|
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
||||||
|
return super(AnymailRequestsBackend, self)._send(message)
|
||||||
|
|
||||||
|
def post_to_esp(self, payload, message):
|
||||||
|
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||||
|
|
||||||
|
payload is the result of build_message_payload
|
||||||
|
message is the original EmailMessage
|
||||||
|
return should be a requests.Response
|
||||||
|
|
||||||
|
Can raise AnymailRequestsAPIError for HTTP errors in the post
|
||||||
|
"""
|
||||||
|
params = payload.get_request_params(self.api_url)
|
||||||
|
response = self.session.request(**params)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def deserialize_response(self, response, payload, message):
|
||||||
|
"""Return parsed ESP API response
|
||||||
|
|
||||||
|
Can raise AnymailRequestsAPIError if response is unparsable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except ValueError:
|
||||||
|
raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
|
||||||
|
email_message=message, payload=payload, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestsPayload(BasePayload):
|
||||||
|
"""Abstract Payload for AnymailRequestsBackend"""
|
||||||
|
|
||||||
|
def __init__(self, message, defaults, backend,
|
||||||
|
method="POST", params=None, data=None,
|
||||||
|
headers=None, files=None, auth=None):
|
||||||
|
self.method = method
|
||||||
|
self.params = params
|
||||||
|
self.data = data
|
||||||
|
self.headers = headers
|
||||||
|
self.files = files
|
||||||
|
self.auth = auth
|
||||||
|
super(RequestsPayload, self).__init__(message, defaults, backend)
|
||||||
|
|
||||||
|
def get_request_params(self, api_url):
|
||||||
|
"""Returns a dict of requests.request params that will send payload to the ESP.
|
||||||
|
|
||||||
|
:param api_url: the base api_url for the backend
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
api_endpoint = self.get_api_endpoint()
|
||||||
|
if api_endpoint is not None:
|
||||||
|
url = urljoin(api_url, api_endpoint)
|
||||||
|
else:
|
||||||
|
url = api_url
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
method=self.method,
|
||||||
|
url=url,
|
||||||
|
params=self.params,
|
||||||
|
data=self.serialize_data(),
|
||||||
|
headers=self.headers,
|
||||||
|
files=self.files,
|
||||||
|
auth=self.auth,
|
||||||
|
# json= is not here, because we prefer to do our own serialization
|
||||||
|
# to provide extra context in error messages
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_api_endpoint(self):
|
||||||
|
"""Returns a str that should be joined to the backend's api_url for sending this payload."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def serialize_data(self):
|
||||||
|
"""Performs any necessary serialization on self.data, and returns the result."""
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
def serialize_json(self, data):
|
||||||
|
"""Returns data serialized to json, raising appropriate errors.
|
||||||
|
|
||||||
|
Useful for implementing serialize_data in a subclass,
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return json.dumps(data)
|
||||||
|
except TypeError as err:
|
||||||
|
# Add some context to the "not JSON serializable" message
|
||||||
|
raise AnymailSerializationError(orig_err=err, email_message=self.message,
|
||||||
|
backend=self.backend, payload=self)
|
||||||
@@ -3,7 +3,7 @@ from datetime import date, datetime
|
|||||||
from ..exceptions import AnymailRequestsAPIError, AnymailRecipientsRefused
|
from ..exceptions import AnymailRequestsAPIError, AnymailRecipientsRefused
|
||||||
from ..utils import last, combine, get_anymail_setting
|
from ..utils import last, combine, get_anymail_setting
|
||||||
|
|
||||||
from .base import AnymailRequestsBackend, RequestsPayload
|
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||||
|
|
||||||
|
|
||||||
class MandrillBackend(AnymailRequestsBackend):
|
class MandrillBackend(AnymailRequestsBackend):
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
from requests import HTTPError
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
try:
|
||||||
|
from requests import HTTPError
|
||||||
|
except ImportError:
|
||||||
|
# Backends that don't use requests aren't required to have it installed
|
||||||
|
# (and could never raise an AnymailRequestsAPIError)
|
||||||
|
class HTTPError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AnymailError(Exception):
|
class AnymailError(Exception):
|
||||||
@@ -121,3 +130,12 @@ class AnymailSerializationError(AnymailError, TypeError):
|
|||||||
if orig_err is not None:
|
if orig_err is not None:
|
||||||
message += "\n%s" % str(orig_err)
|
message += "\n%s" % str(orig_err)
|
||||||
super(AnymailSerializationError, self).__init__(message, *args, **kwargs)
|
super(AnymailSerializationError, self).__init__(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# This deliberately doesn't inherit from AnymailError
|
||||||
|
class AnymailImproperlyInstalled(ImproperlyConfigured, ImportError):
|
||||||
|
def __init__(self, missing_package):
|
||||||
|
message = "The %s package is required to use this backend, but isn't installed.\n" \
|
||||||
|
"(Be sure to use `pip install anymail[<backend>]` with your desired backends)" % missing_package
|
||||||
|
super(AnymailImproperlyInstalled, self).__init__(message)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user