Skip to content

Commit

Permalink
Merge pull request #94 from friedcell/tornado
Browse files Browse the repository at this point in the history
More extensibility and Tornado integration
  • Loading branch information
richleland committed Mar 30, 2016
2 parents c4a194d + 9d7a191 commit 411c565
Show file tree
Hide file tree
Showing 17 changed files with 433 additions and 32 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ Patches and suggestions
- Simeon Visser `@svisser <https://github.com/svisser>`_
- `@gnarvaja <https://github.com/gnarvaja>`_
- `@puttu <https://github.com/puttu>`_
- Marko Mrdjenovic `@friedcell <https://github.com/friedcell>`_
- ADD YOURSELF HERE (and link to your github page)
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
wheel
twine
Django>=1.7,<1.10
tornado>=3.2
31 changes: 19 additions & 12 deletions sparkpost/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

from .base import RequestsTransport
from .exceptions import SparkPostException
from .metrics import Metrics
from .recipient_lists import RecipientLists
Expand All @@ -8,31 +9,37 @@
from .transmissions import Transmissions


__version__ = '1.0.5'


def get_api_key():
"Get API key from environment variable"
return os.environ.get('SPARKPOST_API_KEY', None)
__version__ = '1.0.6.dev1'


class SparkPost(object):
TRANSPORT_CLASS = RequestsTransport

def __init__(self, api_key=None, base_uri='https://api.sparkpost.com',
version='1'):
"Set up the SparkPost API client"
if not api_key:
api_key = get_api_key()
api_key = self.get_api_key()
if not api_key:
raise SparkPostException("No API key. Improve message.")

self.base_uri = base_uri + '/api/v' + version
self.api_key = api_key

self.metrics = Metrics(self.base_uri, self.api_key)
self.recipient_lists = RecipientLists(self.base_uri, self.api_key)
self.suppression_list = SuppressionList(self.base_uri, self.api_key)
self.templates = Templates(self.base_uri, self.api_key)
self.transmissions = Transmissions(self.base_uri, self.api_key)
self.metrics = Metrics(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
self.recipient_lists = RecipientLists(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
self.suppression_list = SuppressionList(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
self.templates = Templates(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
self.transmissions = Transmissions(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
# Keeping self.transmission for backwards compatibility.
# Will be removed in a future release.
self.transmission = self.transmissions

def get_api_key(self):
"Get API key from environment variable"
return os.environ.get('SPARKPOST_API_KEY', None)
30 changes: 20 additions & 10 deletions sparkpost/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import requests
import sparkpost

from .exceptions import SparkPostAPIException


class RequestsTransport(object):
def request(self, method, uri, headers, **kwargs):
import requests
response = requests.request(method, uri, headers=headers, **kwargs)
if response.status_code == 204:
return True
if not response.ok:
raise SparkPostAPIException(response)
if 'results' in response.json():
return response.json()['results']
return response.json()


class Resource(object):
def __init__(self, base_uri, api_key):
key = ""

def __init__(self, base_uri, api_key, transport_class=RequestsTransport):
self.base_uri = base_uri
self.api_key = api_key
self.transport = transport_class()

@property
def uri(self):
Expand All @@ -19,14 +34,9 @@ def request(self, method, uri, **kwargs):
'Content-Type': 'application/json',
'Authorization': self.api_key
}
response = requests.request(method, uri, headers=headers, **kwargs)
if response.status_code == 204:
return True
if not response.ok:
raise SparkPostAPIException(response)
if 'results' in response.json():
return response.json()['results']
return response.json()
response = self.transport.request(method, uri, headers=headers,
**kwargs)
return response

def get(self):
raise NotImplementedError
Expand Down
2 changes: 1 addition & 1 deletion sparkpost/django/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ def __init__(self, message):
'type': mimetype
})

return super(SparkPostMessage, self).__init__(formatted)
super(SparkPostMessage, self).__init__(formatted)
15 changes: 12 additions & 3 deletions sparkpost/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ class SparkPostException(Exception):
class SparkPostAPIException(SparkPostException):
"Handle 4xx and 5xx errors from the SparkPost API"
def __init__(self, response, *args, **kwargs):
errors = response.json()['errors']
errors = [e['message'] + ': ' + e.get('description', '')
for e in errors]
errors = None
try:
errors = response.json()['errors']
errors = [e['message'] + ': ' + e.get('description', '')
for e in errors]
except:
pass
if not errors:
errors = [response.text or ""]
self.status = response.status_code
self.response = response
self.errors = errors
message = """Call to {uri} returned {status_code}, errors:
{errors}
Expand Down
8 changes: 4 additions & 4 deletions sparkpost/metrics.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from .base import Resource
from .base import Resource, RequestsTransport


class Metrics(object):
"Wrapper for sub-resources"

def __init__(self, base_uri, api_key):
def __init__(self, base_uri, api_key, transport_class=RequestsTransport):
self.base_uri = "%s/%s" % (base_uri, 'metrics')
self.campaigns = Campaigns(self.base_uri, api_key)
self.domains = Domains(self.base_uri, api_key)
self.campaigns = Campaigns(self.base_uri, api_key, transport_class)
self.domains = Domains(self.base_uri, api_key, transport_class)


class Campaigns(Resource):
Expand Down
18 changes: 18 additions & 0 deletions sparkpost/tornado/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import sparkpost

from .exceptions import SparkPostAPIException
from .base import TornadoTransport
from .transmissions import Transmissions

__all__ = ["SparkPost", "TornadoTransport", "SparkPostAPIException",
"Transmissions"]


class SparkPost(sparkpost.SparkPost):
TRANSPORT_CLASS = TornadoTransport

def __init__(self, *args, **kwargs):
super(SparkPost, self).__init__(*args, **kwargs)
self.transmissions = Transmissions(self.base_uri, self.api_key,
self.TRANSPORT_CLASS)
self.transmission = self.transmissions
31 changes: 31 additions & 0 deletions sparkpost/tornado/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import json
from tornado import gen
from tornado.httpclient import AsyncHTTPClient, HTTPError

from .exceptions import SparkPostAPIException


class TornadoTransport(object):
@gen.coroutine
def request(self, method, uri, headers, **kwargs):
if "data" in kwargs:
kwargs["body"] = kwargs.pop("data")
client = AsyncHTTPClient()
try:
response = yield client.fetch(uri, method=method, headers=headers,
**kwargs)
except HTTPError as ex:
raise SparkPostAPIException(ex.response)
if response.code == 204:
raise gen.Return(True)
if response.code == 200:
result = None
try:
result = json.loads(response.body.decode("utf-8"))
except:
pass
if result:
if 'results' in result:
raise gen.Return(result['results'])
raise gen.Return(result)
raise SparkPostAPIException(response)
31 changes: 31 additions & 0 deletions sparkpost/tornado/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import json

from ..exceptions import SparkPostAPIException as RequestsSparkPostAPIException


class SparkPostAPIException(RequestsSparkPostAPIException):
def __init__(self, response, *args, **kwargs):
errors = None
try:
data = json.loads(response.body.decode("utf-8"))
if data:
errors = data['errors']
errors = [e['message'] + ': ' + e.get('description', '')
for e in errors]
except:
pass
if not errors:
errors = [response.body.decode("utf-8") or ""]
self.status = response.code
self.response = response
self.errors = errors
message = """Call to {uri} returned {status_code}, errors:
{errors}
""".format(
uri=response.effective_url,
status_code=response.code,
errors='\n'.join(errors)
)
super(RequestsSparkPostAPIException, self).__init__(message, *args,
**kwargs)
8 changes: 8 additions & 0 deletions sparkpost/tornado/transmissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from .utils import wrap_future
from ..transmissions import Transmissions as SyncTransmissions


class Transmissions(SyncTransmissions):
def get(self, transmission_id):
results = self._fetch_get(transmission_id)
return wrap_future(results, lambda f: f["transmission"])
14 changes: 14 additions & 0 deletions sparkpost/tornado/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from tornado.concurrent import Future


def wrap_future(future, convert):
wrapper = Future()

def handle_future(future):
try:
wrapper.set_result(convert(future.result()))
except Exception as ex:
wrapper.set_exception(ex)

future.add_done_callback(handle_future)
return wrapper
8 changes: 6 additions & 2 deletions sparkpost/transmissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ def send(self, **kwargs):
results = self.request('POST', self.uri, data=json.dumps(payload))
return results

def _fetch_get(self, transmission_id):
uri = "%s/%s" % (self.uri, transmission_id)
results = self.request('GET', uri)
return results

def get(self, transmission_id):
"""
Get a transmission by ID
Expand All @@ -214,8 +219,7 @@ def get(self, transmission_id):
:returns: the requested transmission if found
:raises: :exc:`SparkPostAPIException` if transmission is not found
"""
uri = "%s/%s" % (self.uri, transmission_id)
results = self.request('GET', uri)
results = self._fetch_get(transmission_id)
return results['transmission']

def list(self):
Expand Down
28 changes: 28 additions & 0 deletions test/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ def test_fail_request():
resource.request('GET', resource.uri)


@responses.activate
def test_fail_wrongjson_request():
responses.add(
responses.GET,
fake_uri,
status=500,
content_type='application/json',
body='{"errors": ["Error!"]}'
)
resource = create_resource()
with pytest.raises(SparkPostAPIException):
resource.request('GET', resource.uri)


@responses.activate
def test_fail_nojson_request():
responses.add(
responses.GET,
fake_uri,
status=500,
content_type='application/json',
body='{"errors": '
)
resource = create_resource()
with pytest.raises(SparkPostAPIException):
resource.request('GET', resource.uri)


def test_fail_get():
resource = create_resource()
with pytest.raises(NotImplementedError):
Expand Down
Empty file added test/tornado/__init__.py
Empty file.

0 comments on commit 411c565

Please sign in to comment.