Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 1ff8d17299b860a18c1be2f8c6bcca01bfa584de 0 parents
Domen Kožar authored
14 .gitignore
@@ -0,0 +1,14 @@
+*.egg-info
+*.mo
+*.pyc
+.installed.cfg
+.mr.developer.cfg
+bin/
+develop-eggs/
+downloads/
+eggs/
+parts/
+var/
+lib/
+local/
+man/
4 .pep8
@@ -0,0 +1,4 @@
+[pep8]
+ignore = E123,E127
+# E123: closnig bracket must match indent of starting bracket
+# E127 continuation line over-indented for visual indent
9 .travis.yml
@@ -0,0 +1,9 @@
+language: python
+python:
+ - 2.5
+ - 2.6
+ - 2.7
+ - pypy
+script:
+ - pip install nose coverage pep8 setuptools-flakes
+ - ./pre-commit-check.sh
9 HISTORY.rst
@@ -0,0 +1,9 @@
+Changelog
+=========
+
+
+0.1 (unreleased)
+----------------
+
+- Initial release.
+ [Domen Kožar]
24 LICENSE
@@ -0,0 +1,24 @@
+pyramid_marrowmailer Copyright (c) 2012, Domen Kožar
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. The name of the author may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2  MANIFEST.in
@@ -0,0 +1,2 @@
+graft src/pyramid_marrowmailer
+include *.cfg *.rst *.in LICENSE
64 README.rst
@@ -0,0 +1,64 @@
+Pyramid integration package for "A highly efficient and modular mail delivery
+framework for Python 2.6+ and 3.1+, formerly TurboMail."
+
+Currently it must be used with `pyramid_tm`, as `Mailer.send` only works
+if `transaction`.commit() succeeded.
+
+INSTALL
+=======
+
+As always::
+
+ $ env/bin/easy_install pyramid_marrowmailer
+
+
+USAGE
+=====
+
+If you have package installed, you can configure it in Pyramid like always::
+
+ config.include('pyramid_marrowmailer')
+
+All settings `marrow.mailer` expects are prefixed with `mail.`. If you want
+to use different prefix, set it with `pyramid_marrowmailer.prefix`.
+
+To see what options `marrow.mailer` support, see
+`the documentation <https://github.com/marrow/marrow.mailer>`_. Note that
+boolean options need a `.on` suffix. For example `mail.transport.debug.on = true`.
+Options that need to be converted to integer, add `int` suffix. For example
+`mail.transport.port.int = 1337`.
+
+`pyramid_marrowmailer` calls `Mailer.start` when `config.include('pyramid_marrowmailer')`
+is called. `atexit` is used to register `Mailer.stop` to shutdown when wsgi server will exit.
+
+Note that `pyramid_marrowmailer` subclasses `marrow.mailer.Mailer` to provide support for
+`transaction`. Class is importable from `pyramid_marrowmailer.TransactionMailer`
+
+You can accces `pyramid_marrowmailer.TransactionMailer` instance in two ways::
+
+ mailer = request.mailer
+ message = mailer.new()
+ message.send()
+
+Or::
+
+ from pyramid_marrowmailer import get_mailer
+ mailer = get_mailer(request)
+ message = mailer.new()
+ message.send()
+
+EXAMPLE
+=======
+
+TESTING
+=======
+
+::
+ $ pip install nose coverage pep8 setuptools-flakes
+ $ ./pre-commit-check.sh
+
+
+TODO
+====
+
+- Add support to use `marrow.mailer` without usage of `transaction`
18 pre-commit-check.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# TODO: stash changes before doing tests: http://codeinthehole.com/writing/tips-for-using-a-git-pre-commit-hook/
+
+function handle_exit {
+ if [ $? -ne 0 ]; then
+ EXITCODE=1
+ fi
+}
+
+echo '====== Running tests ========='
+bin/nosetests; handle_exit
+
+echo '====== Running PyFlakes ======'
+bin/python setup.py flakes; handle_exit
+
+echo '====== Running pep8 =========='
+bin/pep8 src/pyramid_marrowmailer; handle_exit
+bin/pep8 *.py; handle_exit
10 setup.cfg
@@ -0,0 +1,10 @@
+[egg_info]
+tag_build =
+tag_date = 0
+
+[nosetests]
+match=^test
+nocapture=1
+cover-package=pyramid_marrowmailer
+with-coverage=1
+cover-erase=1
50 setup.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+import os
+
+from setuptools import setup
+from setuptools import find_packages
+
+
+def read(*rnames):
+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+
+setup(name='pyramid_marrowmailer',
+ version='0.1',
+ description='Pyramid integration package for marrow.mailer,'
+ ' formerly known as TurboMail',
+ long_description=read('README.rst') +
+ read('HISTORY.rst') +
+ read('LICENSE'),
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ ],
+ keywords='web wsgi pylons pyramid',
+ author='',
+ author_email='domen@dev.si',
+ url='https://github.com/iElectric/pyramid_marrowmailer',
+ license='BSD',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ install_requires=[
+ 'pyramid',
+ 'pyramid_tm',
+ 'marrow.mailer',
+ 'setuptools',
+ 'transaction',
+ ],
+ extras_require={
+ 'test': [
+ 'nose',
+ 'coverage',
+ 'setuptools-flakes',
+ 'pep8',
+ ],
+ },
+ entry_points="""
+ """,
+ include_package_data=True,
+ zip_safe=False,
+ )
96 src/pyramid_marrowmailer/__init__.py
@@ -0,0 +1,96 @@
+import atexit
+
+import transaction
+from marrow.mailer import Mailer
+from pyramid.settings import asbool
+from zope.interface import Interface
+
+
+class IMarrowMailer(Interface):
+ pass
+
+
+class MailDataManager(object):
+ """Stolen from repoze/sendmail/delivery.py under ZPL 1.1 license"""
+
+ def __init__(self, callable, args, kwargs):
+ self.callable = callable
+ self.args = args
+ self.kwargs = kwargs
+ # Use the default thread transaction manager.
+ self.transaction_manager = transaction.manager
+
+ def commit(self, transaction):
+ pass
+
+ def abort(self, transaction):
+ pass
+
+ def sortKey(self):
+ return id(self)
+
+ # No subtransaction support.
+ def abort_sub(self, transaction):
+ pass # pragma NO COVERAGE
+
+ commit_sub = abort_sub
+
+ def beforeCompletion(self, transaction):
+ pass # pragma NO COVERAGE
+
+ afterCompletion = beforeCompletion
+
+ def tpc_begin(self, transaction, subtransaction=False):
+ assert not subtransaction
+
+ def tpc_vote(self, transaction):
+ pass
+
+ def tpc_finish(self, transaction):
+ self.callable(*self.args, **self.kwargs)
+
+ tpc_abort = abort
+
+
+class TransactionMailer(Mailer):
+ """Mailer that obeys zope transaction for sending emails"""
+
+ def send(self, *a, **kw):
+ send_ = super(TransactionMailer, self).send
+ transaction.get().join(MailDataManager(send_, a, kw))
+
+
+def includeme(config):
+ """Configure marrow.mailer"""
+ settings = config.registry.settings
+ prefix = settings.get('pyramid_marrowmailer.prefix', 'mail.').rstrip('.')
+
+ # handle boolean options and int options .digit .on
+ mailer_config = dict(filter(lambda d: d[0].startswith(prefix),
+ settings.items()))
+ for key, value in dict(mailer_config).items():
+ if key.endswith('.on'):
+ mailer_config[key[:-3]] = asbool(value)
+ if key.endswith('.int'):
+ mailer_config[key[:-4]] = int(value)
+
+ # bugfix for https://github.com/marrow/marrow.mailer/issues/45
+ manager = '%s.manager.use' % prefix
+ if manager not in mailer_config:
+ mailer_config[manager] = 'immediate'
+
+ mailer = TransactionMailer(mailer_config, prefix)
+ mailer.start()
+
+ config.registry.registerUtility(mailer, IMarrowMailer)
+ config.set_request_property(get_mailer, "mailer", reify=True)
+
+ # shutdown mailer when process stops
+ atexit.register(lambda: mailer.stop())
+
+
+def get_mailer(request):
+ """Obtain a mailer previously registered via
+ ``config.include('pyramid_marrrowmailer')``.
+ """
+ return request.registry.getUtility(IMarrowMailer)
119 src/pyramid_marrowmailer/tests.py
@@ -0,0 +1,119 @@
+from __future__ import with_statement
+import unittest
+import logging
+
+from pyramid import testing
+
+
+class ListHandler(logging.Handler):
+ """Logging handler for testing"""
+
+ debug = []
+ warning = []
+ info = []
+ error = []
+ critical = []
+
+ def emit(self, record):
+ getattr(self.__class__, record.levelname.lower())\
+ .append(record.getMessage())
+
+ @classmethod
+ def reset(cls):
+ for attr in dir(cls):
+ if isinstance(getattr(cls, attr), list):
+ setattr(cls, attr, [])
+
+
+class BaseFunctionalTest(unittest.TestCase):
+
+ def setUp(self):
+ self.request = testing.DummyRequest()
+ self.config = testing.setUp(request=self.request)
+
+ def tearDown(self):
+ testing.tearDown()
+
+
+class get_mailerTest(BaseFunctionalTest):
+ def test_it(self):
+ from pyramid_marrowmailer import get_mailer, TransactionMailer
+ self.config.registry.settings['mail.transport.use'] = 'mock'
+ self.config.include('pyramid_marrowmailer')
+ mailer = get_mailer(self.request)
+ self.assertTrue(isinstance(mailer, TransactionMailer))
+
+
+class includemeTest(BaseFunctionalTest):
+ def test_boolean_option(self):
+ from pyramid_marrowmailer import get_mailer
+ self.config.registry.settings['mail.transport.use'] = 'smtp'
+ self.config.registry.settings['mail.transport.debug.on'] = 'true'
+ self.config.include('pyramid_marrowmailer')
+ self.assertTrue(get_mailer(self.request).config['transport.debug'])
+
+ def test_digit_option(self):
+ from pyramid_marrowmailer import get_mailer
+ self.config.registry.settings['mail.transport.use'] = 'smtp'
+ self.config.registry.settings['mail.transport.port.int'] = '100'
+ self.config.include('pyramid_marrowmailer')
+ self.assertEqual(get_mailer(self.request).config['transport.port'],
+ 100)
+
+ def test_mailer_config_prefix(self):
+ from pyramid_marrowmailer import get_mailer
+ settings = self.config.registry.settings
+ settings['pyramid_marrowmailer.prefix'] = 'foobar.'
+ settings['foobar.transport.use'] = 'smtp'
+ self.config.include('pyramid_marrowmailer')
+ self.assertEqual(get_mailer(self.request).config['transport.use'],
+ 'smtp')
+
+
+class transactionTest(BaseFunctionalTest):
+ def configure(self):
+ settings = self.config.registry.settings
+ settings['mail.transport.use'] = 'logging'
+ settings['mail.message.author'] = 'foobar@foo.com'
+ self.config.include('pyramid_marrowmailer')
+
+ logging.basicConfig()
+ root_logger = logging.getLogger()
+ self.handler = ListHandler()
+ self.handler.reset()
+ root_logger.addHandler(self.handler)
+
+ def test_send(self):
+ self.configure()
+ from pyramid_marrowmailer import get_mailer
+ mailer = get_mailer(self.request)
+
+ import transaction
+ with transaction.manager:
+ message = mailer.new()
+ message.subject = "foobar"
+ message.to = "foobar@bar.com"
+ message.plain = "hi"
+ message.send()
+ self.assertEqual(self.handler.info, [])
+
+ self.assertTrue('DELIVER' in self.handler.info[1])
+
+ def test_send_abort(self):
+ self.configure()
+ from pyramid_marrowmailer import get_mailer
+ mailer = get_mailer(self.request)
+
+ import transaction
+ try:
+ with transaction.manager:
+ message = mailer.new()
+ message.subject = "foobar2"
+ message.to = "foobar2@bar.com"
+ message.plain = "hi"
+ message.send()
+ raise ValueError
+ except ValueError:
+ pass
+
+ self.assertEqual(self.handler.info, [])
Please sign in to comment.
Something went wrong with that request. Please try again.