mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41: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.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 .._version import __version__
|
||||
|
||||
|
||||
class AnymailBaseBackend(BaseEmailBackend):
|
||||
@@ -180,78 +173,6 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
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):
|
||||
# attr, combiner, converter
|
||||
base_message_attrs = (
|
||||
@@ -407,62 +328,3 @@ class BasePayload(object):
|
||||
# ESP-specific payload construction
|
||||
def set_esp_extra(self, 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 ..utils import last, combine, get_anymail_setting
|
||||
|
||||
from .base import AnymailRequestsBackend, RequestsPayload
|
||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
|
||||
|
||||
class MandrillBackend(AnymailRequestsBackend):
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
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):
|
||||
@@ -121,3 +130,12 @@ class AnymailSerializationError(AnymailError, TypeError):
|
||||
if orig_err is not None:
|
||||
message += "\n%s" % str(orig_err)
|
||||
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