Skip to content

Commit

Permalink
Merge branch 'release/0.0.8'
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Mathie committed Jun 2, 2016
2 parents 4cb8bbe + d5e078e commit 94d7d66
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 39 deletions.
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ python:
- "2.7"

env:
- MAILGUN_API_KEY=testtesttest
global:
- MAILGUN_API_KEY=testtesttest
- secure: "QWNGqrTcNUIdtpBKz1KMsx54SZ5OkQ1eK8SR4iJag4I3UXhT7MIwtbBrWyIEQvoC2in23gp5lPPiG842lPgKebOz+8GyJHYaE7SKPjPdpRfxPJSoDrJagArKNcsZrcZJkAr9jIpqV6Ap7p0TzzOWXsqYDbmLWxgrJOLHTLHi9QZc71vjP4wRhRBtz24xcUgSsRRUrM56FrxSy27zoem4AeJwRRVB52d1HwDbcHtbJ/3taVHFd4WLbdUIbwyOvXX3RGqVv2TxX9YoH+1S18mSoKy+OWulOQyrHc8pjPkz+cNyCYXE2Pu3m97e5c+NzFLsnfqYyKwROM49qGOvXbY0tziVYUAvgoJ1P+fsShKH5WKhv9J917R5QLf12iWKPI3/apBhXb5yGoFOerSxMUgJz4hSuKGTbg7nJ2hWJZVop7+c2LiSbtahghhmjJwlVzNSzTb6X6UayEgVlaqSF7wyq5pJmMXZ7fDxoooTyu4iDjRwYzxH1OrUWYz2Q9pHK7YAf9dHOgnrqaI6f+HQ9qt9auG9pGQ7Ep7tQVcikvWLZEgyqYLClagazqwoJImPrLrip3JVnb4lTudiwn2jKHYVgSjfEJn28aBf9ArtBpIvtnl5Rpe7fHrfeape5zUZlxC0lZ2sYWkoef/KooYWKk1iXOPA59RmswE6G8OZ+KmrV9s="

install:
- pip install codeclimate-test-reporter
- pip install -r requirements.txt
- pip install -r requirements_test.txt

script: coverage run --source=. -m unittest discover

after_success:
- coveralls
- codeclimate-test-reporter
102 changes: 82 additions & 20 deletions flask_mailgun.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
import hashlib
import hmac
import os
import shutil

import json
import tempfile
from collections import defaultdict
from decorator import decorator
from functools import wraps
from threading import Thread
from multiprocessing import Pool
from werkzeug.utils import secure_filename
from werkzeug.datastructures import FileStorage


class MailGunException(Exception):
pass
Expand All @@ -40,13 +47,45 @@ class MailGunException(Exception):
MAILGUN_API_URL = 'https://api.mailgun.net/v3'


def async_pool(pool_size):
def wrapper(func):
pool = Pool(pool_size)

@wraps(func)
def inner(*args, **kwargs):
return pool.apply_async(func, args=args, kwds=kwargs)
return inner
return wrapper


@decorator
def sync(f, *args, **kwargs):
return f(*args, **kwargs)


@decorator
def async(f, *args, **kwargs):
# this is not thread safe at the moment
# TODO consider using celery or multiprocesing pool
thread = Thread(target=f, args=args, kwargs=kwargs)
thread.start()
return thread
def attachment_decorator(f, email, filename):
"""Converts a file back into a FileStorage Object"""
with open(filename, 'r') as file:
attachment = FileStorage(stream=file,
filename=filename)
result = f(email, attachment)
return result


def clean_up(results, tempdir):
"""Clean up after an email is procesed
Take the returned Async Results and wait for all results to return
before removing temporary folder
"""
for result in results:
try:
result.wait()
except AttributeError:
"""Not Async"""
shutil.rmtree(tempdir)
return 1


class MailGun(object):
Expand All @@ -55,7 +94,6 @@ class MailGun(object):
mailgun_api = None

auto_reply = True
run_async = True
logger = None

def __init__(self, app=None):
Expand All @@ -65,6 +103,12 @@ def __init__(self, app=None):
def init_app(self, app):
self.app = app
self.mailgun_api = MailGunAPI(app.config)
self.allowed_extensions = app.config.get('ALLOWED_EXTENSIONS',
ALL_EXTENSIONS)
self.callback_handeler = app.config.get('MAILGUN_CALLBACK_HANDELER',
sync)
self.async = async_pool(app.config.get('MAILGUN_BG_PROCESSES', 4))

self._on_receive = []
self._on_attachment = []

Expand Down Expand Up @@ -117,7 +161,7 @@ def on_receive(self, func):
`@mailgun.on_receive
def process_email(email)`
"""
self._on_receive.append(func)
self._on_receive.append(self.callback_handeler(func))
return func

def on_attachment(self, func):
Expand All @@ -126,9 +170,28 @@ def on_attachment(self, func):
`@mailgun.on_attachment
def process_attachment(email, filestorage)`
"""
self._on_attachment.append(func)
new_func = self.callback_handeler(attachment_decorator(func))
self._on_attachment.append(new_func)
return func

def file_allowed(self, filename):
return '.' in filename and \
filename.rsplit('.', 1)[1] in self.allowed_extensions

def get_attachments(self, request):
files = request.files.values()
attachments = [att for att in files if self.file_allowed(att.filename)]
return attachments

def save_attachments(self, attachments, tempdir=None):
if not tempdir:
tempdir = tempfile.mkdtemp()
filenames = [secure_filename(att.filename) for att in attachments]
filenames = [os.path.join(tempdir, name) for name in filenames]
for (filename, attachment) in zip(filenames, attachments):
attachment.save(filename)
return filenames

def process_email(self, request):
"""Function to pass to endpoint for processing incoming email post
Expand All @@ -137,19 +200,18 @@ def process_email(self, request):
email = request.form
self.mailgun_api.verify_email(email)
# Process the attachments
for func in self._on_attachment:
if self.run_async:
func = async(func)
for attachment in request.files.values():
attachment.filename = secure_filename(attachment.filename)
func(email, attachment)
# data = attachment.stream.read()
# with open(attachment.filename, "w") as f:
# f.write(data)
tempdir = tempfile.mkdtemp()
attachments = self.get_attachments(request)
filenames = self.save_attachments(attachments, tempdir)
results = [func(email, attachment)
for func in self._on_attachment
for attachment in filenames]

cleanup = Thread(target=clean_up, args=(results, tempdir))
cleanup.start()

# Process the email
for func in self._on_receive:
if self.run_async:
func = async(func)
func(email)
# log and notify
self.__log_status(request)
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@
@author: richard
"""
import os


def get_attachment():
filename = "test_attachment.txt"
fixture_dir = os.path.dirname(__file__)
f_name = os.path.join(fixture_dir, filename)
file_stream = open(f_name, "r")
return (filename, file_stream)
7 changes: 2 additions & 5 deletions tests/fixtures/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from flask import Request
import hashlib
import hmac
import os
import random
import string
from time import time
from tests.fixtures import get_attachment

url_safe_chars = string.lowercase+string.digits+string.uppercase

Expand Down Expand Up @@ -47,10 +47,7 @@ def make_email():

def attach_file(email):
"""generate request with attachment"""
filename = "test_attachment.txt"
fixture_dir = os.path.dirname(__file__)
f_name = os.path.join(fixture_dir, filename)
file_stream = open(f_name, "r")
(filename, file_stream) = get_attachment()
attachment = dict(filename=filename,
file=file_stream)
email.update({"attachment-count": 1})
Expand Down
41 changes: 41 additions & 0 deletions tests/test_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# import os
import time
# from flask_mailgun import async
from multiprocessing import Pool # , active_children
from functools import wraps
import unittest


# function decorator
def async_pool(pool_size):

def wrapper(func):
pool = Pool(pool_size)

@wraps(func)
def inner(*args, **kwargs):
return pool.apply_async(func, args=args, kwds=kwargs)
return inner
return wrapper


def runner(fun):
results = [fun(i) for i in xrange(20)]
for result in results:
result.wait()
result.get()


def foo(arg):
time.sleep(0.1)
return arg


class AsyncTest(unittest.TestCase):

def test_async(self):
async_foo = async_pool(4)(foo)
runner(async_foo)

if __name__ == '__main__':
unittest.main()
24 changes: 12 additions & 12 deletions tests/test_flask_mailgun.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ def test_send_simple_message(self):


class ReceiveMessageTest(MailgunTestBase):
# def __init__(self):
# # Add on_receive and on_attachment functionality to the App
# @self.mailgun.on_receive
# def

def test_email_verify(self):
email = make_email()
Expand All @@ -84,7 +80,6 @@ def test_receive_message(self):
request = make_email_request(self.mailgun)
# files = request.pop('files',[])
self.mailgun.create_route('/upload')
self.mailgun.run_async = False

response = self.appclient.post('/upload', data=request)
self.assertEqual(response.status_code, 200)
Expand All @@ -95,7 +90,6 @@ class ReceiveMessageCallbacksTest(MailgunTestBase):

def setUp(self):
super(ReceiveMessageCallbacksTest, self).setUp()
self.mailgun.run_async = False
self.mailgun.create_route('/upload')

self.email = make_email_request(self.mailgun)
Expand All @@ -111,6 +105,7 @@ def receive_email_func(*args, **kwargs):

@self.mailgun.on_attachment
def attachment_func(email, attachment):
# print "processing on", os.getpid()
responce = self.attachment_mock(email, attachment)
data = attachment.read()
len(data)
Expand All @@ -131,31 +126,36 @@ def test_receive_message(self):
print "reveved email"


class ReceiveMessageAsyncTest(ReceiveMessageSyncTest):
class ReceiveMessageAsyncTest(ReceiveMessageCallbacksTest):

def setUp(self):
super(ReceiveMessageAsyncTest, self).setUp()
self.email1 = make_email_request(self.mailgun)
self.email2 = make_email_request(self.mailgun)
self.mailgun.run_async = True
# re register callbacks as async
self.mailgun.callback_handeler = self.mailgun.async
callbacks = self.mailgun._on_attachment
self.mailgun._on_attachment = []
for callback in callbacks:
self.mailgun.on_attachment(callback)

def test_receive_2_messages(self):
response = self.appclient.post('/upload', data=self.email1)
self.assertEqual(response.status_code, 200)
response = self.appclient.post('/upload', data=self.email2)
self.assertEqual(response.status_code, 200)
time.sleep(1)
self.assertEqual(self.receve_email_mock.call_count, 2)
self.assertEqual(self.attachment_mock.call_count, 2)
# self.assertEqual(self.receve_email_mock.call_count, 2)
# self.assertEqual(self.attachment_mock.call_count, 2)
print "reveved 2 emails"

def test_receive_100_messages(self):
for i in xrange(100):
email = make_email_request(self.mailgun)
response = self.appclient.post('/upload', data=email)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.receve_email_mock.call_count, 100)
self.assertEqual(self.attachment_mock.call_count, 100)
# self.assertEqual(self.receve_email_mock.call_count, 100)
# self.assertEqual(self.attachment_mock.call_count, 100)
print "reveved 100 emails"

if __name__ == '__main__':
Expand Down
43 changes: 43 additions & 0 deletions tests/test_save_attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
Created on Thu Jun 2 12:08:14 2016
@author: richard
"""
import unittest
import os
import shutil
import tempfile
from werkzeug import FileStorage
from tests.fixtures import get_attachment
from test_flask_mailgun import MailgunTestBase


class SaveAttachmentTest(MailgunTestBase):
def setUp(self):
super(SaveAttachmentTest, self).setUp()
(filename, file_stream) = get_attachment()
self.attachment = FileStorage(stream=file_stream,
filename=filename)

def tearDown(self):
super(SaveAttachmentTest, self).tearDown()
self.attachment.close()

def test_fileallowed(self):
self.assertTrue(self.mailgun.file_allowed('test.txt'))
self.assertFalse(self.mailgun.file_allowed('bob'))

def test_save_attachments(self):
testdir = tempfile.mkdtemp()
self.attachment.seek(0)
filenames = self.mailgun.save_attachments([self.attachment], testdir)
filenames = [os.path.basename(filename) for filename in filenames]
self.assertTrue(set(os.listdir(testdir)) == set(filenames))
self.assertEqual(len(os.listdir(testdir)), 1)
shutil.rmtree(testdir)
with self.assertRaises(OSError):
os.listdir(testdir)

if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ def _safe_int(string):
return string


__version__ = '0.0.7'
__version__ = '0.0.8'
VERSION = tuple(_safe_int(x) for x in __version__.split('.'))

0 comments on commit 94d7d66

Please sign in to comment.