mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Allow requests session customization, document use for automatic retries
* Refactor create_session() out of AnymailRequestsBackend * Document automatic retries with Requests
This commit is contained in:
@@ -30,6 +30,13 @@ vNext
|
|||||||
|
|
||||||
*Unreleased changes*
|
*Unreleased changes*
|
||||||
|
|
||||||
|
Features
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
* Support customizing the requests.Session for requests-based backends,
|
||||||
|
and document how this can be used to mount an adapter that simplifies
|
||||||
|
automatic retry logic. (Thanks to `@dgilmanAIDENTIFIED`_.)
|
||||||
|
|
||||||
Fixes
|
Fixes
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
@@ -1340,6 +1347,7 @@ Features
|
|||||||
.. _@costela: https://github.com/costela
|
.. _@costela: https://github.com/costela
|
||||||
.. _@coupa-anya: https://github.com/coupa-anya
|
.. _@coupa-anya: https://github.com/coupa-anya
|
||||||
.. _@decibyte: https://github.com/decibyte
|
.. _@decibyte: https://github.com/decibyte
|
||||||
|
.. _@dgilmanAIDENTIFIED: https://github.com/dgilmanAIDENTIFIED
|
||||||
.. _@dominik-lekse: https://github.com/dominik-lekse
|
.. _@dominik-lekse: https://github.com/dominik-lekse
|
||||||
.. _@erikdrums: https://github.com/erikdrums
|
.. _@erikdrums: https://github.com/erikdrums
|
||||||
.. _@ewingrj: https://github.com/ewingrj
|
.. _@ewingrj: https://github.com/ewingrj
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
|||||||
return False # already exists
|
return False # already exists
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session = requests.Session()
|
self.session = self.create_session()
|
||||||
except requests.RequestException:
|
except Exception:
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise
|
raise
|
||||||
else:
|
|
||||||
self.session.headers["User-Agent"] = "django-anymail/{version}-{esp} {orig}".format(
|
|
||||||
esp=self.esp_name.lower(), version=__version__,
|
|
||||||
orig=self.session.headers.get("User-Agent", ""))
|
|
||||||
if self.debug_api_requests:
|
|
||||||
self.session.hooks['response'].append(self._dump_api_request)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -57,6 +52,23 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
|||||||
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
||||||
return super()._send(message)
|
return super()._send(message)
|
||||||
|
|
||||||
|
def create_session(self):
|
||||||
|
"""Create the requests.Session object used by this instance of the backend.
|
||||||
|
If subclassed, you can modify the Session returned from super() to give
|
||||||
|
it your own configuration.
|
||||||
|
|
||||||
|
This must return an instance of requests.Session."""
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
session.headers["User-Agent"] = "django-anymail/{version}-{esp} {orig}".format(
|
||||||
|
esp=self.esp_name.lower(), version=__version__,
|
||||||
|
orig=session.headers.get("User-Agent", ""))
|
||||||
|
|
||||||
|
if self.debug_api_requests:
|
||||||
|
session.hooks['response'].append(self._dump_api_request)
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
def post_to_esp(self, payload, message):
|
def post_to_esp(self, payload, message):
|
||||||
"""Post payload to ESP send API endpoint, and return the raw response.
|
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ intersphinx_mapping = {
|
|||||||
'python': ('https://docs.python.org/3.10', None),
|
'python': ('https://docs.python.org/3.10', None),
|
||||||
'django': ('https://docs.djangoproject.com/en/stable/', 'https://docs.djangoproject.com/en/stable/_objects/'),
|
'django': ('https://docs.djangoproject.com/en/stable/', 'https://docs.djangoproject.com/en/stable/_objects/'),
|
||||||
'requests': ('https://requests.readthedocs.io/en/stable/', None),
|
'requests': ('https://requests.readthedocs.io/en/stable/', None),
|
||||||
|
'urllib3': ('https://urllib3.readthedocs.io/en/stable/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,3 +23,46 @@ transient ESP errors depends on your Django project:
|
|||||||
In addition to handling connectivity issues, either of these approaches also has the advantage
|
In addition to handling connectivity issues, either of these approaches also has the advantage
|
||||||
of moving email sending to a background thread. This is a best practice for sending email from
|
of moving email sending to a background thread. This is a best practice for sending email from
|
||||||
Django, as it allows your web views to respond faster.
|
Django, as it allows your web views to respond faster.
|
||||||
|
|
||||||
|
Automatic retries
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Backends that use :pypi:`requests` for network calls can configure its built-in retry
|
||||||
|
functionality. Subclass the Anymail backend and mount instances of
|
||||||
|
:class:`~requests.adapters.HTTPAdapter` and :class:`~urllib3.util.Retry` configured with
|
||||||
|
your settings on the :class:`~requests.Session` object in `create_session()`.
|
||||||
|
|
||||||
|
Automatic retries aren't a substitute for sending emails in a background thread, they're
|
||||||
|
a way to simplify your retry logic within the worker. Be aware that retrying `read` and `other`
|
||||||
|
failures may result in sending duplicate emails. Requests will only attempt to retry idempotent
|
||||||
|
HTTP verbs by default, you may need to whitelist the verbs used by your backend's API in
|
||||||
|
`allowed_methods` to actually get any retries. It can also automatically retry error HTTP
|
||||||
|
status codes for you but you may need to configure `status_forcelist` with the error HTTP status
|
||||||
|
codes used by your backend provider.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import anymail.backends.mandrill
|
||||||
|
from django.conf import settings
|
||||||
|
import requests.adapters
|
||||||
|
|
||||||
|
|
||||||
|
class RetryableMandrillEmailBackend(anymail.backends.mandrill.EmailBackend):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
retry = requests.adapters.Retry(
|
||||||
|
total=settings.EMAIL_TOTAL_RETRIES,
|
||||||
|
connect=settings.EMAIL_CONNECT_RETRIES,
|
||||||
|
read=settings.EMAIL_READ_RETRIES,
|
||||||
|
status=settings.EMAIL_HTTP_STATUS_RETRIES,
|
||||||
|
other=settings.EMAIL_OTHER_RETRIES,
|
||||||
|
allowed_methods=False, # Retry all HTTP verbs
|
||||||
|
status_forcelist=settings.EMAIL_HTTP_STATUS_RETRYABLE,
|
||||||
|
backoff_factor=settings.EMAIL_RETRY_BACKOFF_FACTOR,
|
||||||
|
)
|
||||||
|
self.retryable_adapter = requests.adapters.HTTPAdapter(max_retries=retry)
|
||||||
|
|
||||||
|
def create_session(self):
|
||||||
|
session = super().create_session()
|
||||||
|
session.mount("https://", self.retryable_adapter)
|
||||||
|
return session
|
||||||
|
|||||||
Reference in New Issue
Block a user