SparkPost: add SPARKPOST_API_URL setting to allow SparkPost EU, etc.

Closes #100
This commit is contained in:
medmunds
2018-04-06 12:57:39 -07:00
parent 64bb3b6098
commit 05f11db4ce
3 changed files with 84 additions and 3 deletions

View File

@@ -24,8 +24,18 @@ class EmailBackend(AnymailBaseBackend):
# SPARKPOST_API_KEY is optional - library reads from env by default # SPARKPOST_API_KEY is optional - library reads from env by default
self.api_key = get_anymail_setting('api_key', esp_name=self.esp_name, self.api_key = get_anymail_setting('api_key', esp_name=self.esp_name,
kwargs=kwargs, allow_bare=True, default=None) kwargs=kwargs, allow_bare=True, default=None)
# SPARKPOST_API_URL is optional - default is set by library;
# if provided, must be a full SparkPost API endpoint, including "/v1" if appropriate
api_url = get_anymail_setting('api_url', esp_name=self.esp_name, kwargs=kwargs, default=None)
extra_sparkpost_params = {}
if api_url is not None:
if api_url.endswith("/"):
api_url = api_url[:-1]
extra_sparkpost_params['base_uri'] = _FullSparkPostEndpoint(api_url)
try: try:
self.sp = SparkPost(self.api_key) # SparkPost API instance self.sp = SparkPost(self.api_key, **extra_sparkpost_params) # SparkPost API instance
except SparkPostException as err: except SparkPostException as err:
# This is almost certainly a missing API key # This is almost certainly a missing API key
raise AnymailConfigurationError( raise AnymailConfigurationError(
@@ -209,3 +219,33 @@ class SparkPostPayload(BasePayload):
# ESP-specific payload construction # ESP-specific payload construction
def set_esp_extra(self, extra): def set_esp_extra(self, extra):
self.params.update(extra) self.params.update(extra)
class _FullSparkPostEndpoint(str):
"""A string-like object that allows using a complete SparkPost API endpoint url as base_uri:
sp = SparkPost(api_key, base_uri=_FullSparkPostEndpoint('https://api.sparkpost.com/api/labs'))
Works around SparkPost.__init__ code `self.base_uri = base_uri + '/api/v' + version`,
which makes it difficult to simply copy and paste full API endpoints from SparkPost's docs
(https://developers.sparkpost.com/api/index.html#header-api-endpoints) -- and completely
prevents using the labs API endpoint (which has no "v" in it).
Should work with all python-sparkpost releases through at least v1.3.6.
"""
_expect = ['/api/v', '1'] # ignore attempts to concatenate these with me (in order)
def __add__(self, other):
expected = self._expect[0]
self._expect = self._expect[1:] # (makes a copy for this instance)
if other == expected:
# ignore this operation
if self._expect:
return self
else:
return str(self) # my work is done; just be a normal str now
else:
# something changed in python-sparkpost; please open an Anymail issue to fix
raise ValueError(
"This version of Anymail is not compatible with this version of python-sparkpost.\n"
"(_FullSparkPostEndpoint(%r) expected %r but got %r)" % (self, expected, other))

View File

@@ -61,6 +61,30 @@ nor ``ANYMAIL_SPARKPOST_API_KEY`` is set.
.. _SparkPost account API keys: https://app.sparkpost.com/account/credentials .. _SparkPost account API keys: https://app.sparkpost.com/account/credentials
.. setting:: ANYMAIL_SPARKPOST_API_URL
.. rubric:: SPARKPOST_API_URL
The `SparkPost API Endpoint`_ to use. This setting is optional; if not provided, Anymail will
use the :pypi:`python-sparkpost` client default endpoint (``"https://api.sparkpost.com/api/v1"``).
Set this to use a SparkPost EU account, or to work with any other API endpoint including
SparkPost Enterprise API and SparkPost Labs.
.. code-block:: python
ANYMAIL = {
...
"SPARKPOST_API_URL": "https://api.eu.sparkpost.com/api/v1", # use SparkPost EU
}
You must specify the full, versioned API endpoint as shown above (not just the base_uri).
This setting only affects Anymail's calls to SparkPost, and will not apply to other code
using :pypi:`python-sparkpost`.
.. _SparkPost API Endpoint: https://developers.sparkpost.com/api/index.html#header-api-endpoints
.. _sparkpost-esp-extra: .. _sparkpost-esp-extra:
esp_extra support esp_extra support

View File

@@ -586,8 +586,8 @@ class SparkPostBackendRecipientsRefusedTests(SparkPostBackendMockAPITestCase):
@override_settings(EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend") @override_settings(EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend")
class SparkPostBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin): class SparkPostBackendConfigurationTests(SimpleTestCase, AnymailTestMixin):
"""Test ESP backend without required settings in place""" """Test various SparkPost client options"""
def test_missing_api_key(self): def test_missing_api_key(self):
with self.assertRaises(AnymailConfigurationError) as cm: with self.assertRaises(AnymailConfigurationError) as cm:
@@ -606,3 +606,20 @@ class SparkPostBackendImproperlyConfiguredTests(SimpleTestCase, AnymailTestMixin
# Poke into implementation details to verify: # Poke into implementation details to verify:
self.assertIsNone(conn.api_key) # Anymail prop self.assertIsNone(conn.api_key) # Anymail prop
self.assertEqual(conn.sp.api_key, 'key_from_environment') # SparkPost prop self.assertEqual(conn.sp.api_key, 'key_from_environment') # SparkPost prop
@override_settings(ANYMAIL={
"SPARKPOST_API_URL": "https://api.eu.sparkpost.com/api/v1",
"SPARKPOST_API_KEY": "example-key",
})
def test_sparkpost_api_url(self):
conn = mail.get_connection() # this init's the backend without sending anything
# Poke into implementation details to verify:
self.assertEqual(conn.sp.base_uri, "https://api.eu.sparkpost.com/api/v1")
# can also override on individual connection (and even use non-versioned labs endpoint)
conn2 = mail.get_connection(api_url="https://api.sparkpost.com/api/labs")
self.assertEqual(conn2.sp.base_uri, "https://api.sparkpost.com/api/labs")
# double-check _FullSparkPostEndpoint won't interfere with additional str ops
self.assertEqual(conn.sp.base_uri + "/transmissions/send",
"https://api.eu.sparkpost.com/api/v1/transmissions/send")