diff --git a/setup.py b/setup.py index 47c9ae6..cf9fe91 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ METADATA = dict( name='yell', - version=yell.__version__, + version="0.5.0", author='Alen Mujezinovic', author_email='flashingpumpkin@gmail.com', description='User notification library with pluggable backends. Compatible with popular frameworks such as Django, Flask, Celery.', diff --git a/yell/__init__.py b/yell/__init__.py index 86c1035..1095ff3 100644 --- a/yell/__init__.py +++ b/yell/__init__.py @@ -1,66 +1,102 @@ -__version__ = "0.3.2" +# -*- coding: utf-8 -*- + +""" + yell + ~~~~~~~~~~~~~ + Pluggable notifications. +""" + +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + +from future import standard_library +standard_library.install_aliases() + +from builtins import * +from builtins import object +from future.utils import with_metaclass + +__version__ = "0.4.0" + + +class Reg(object): + """ Registry mapping notification names to backends """ + + notifications = {} + + def __new__(cls): + obj = super(Reg, cls).__new__(cls) + obj.notifications = {} + return obj + + +registry = Reg() -import registry class MetaNotification(type): - """ + """ Metaclass that stores all notifications in the registry. """ - def __new__(cls, name, bases, attrs): - Notification = super(MetaNotification, cls).__new__(cls, name, bases, attrs) - - if Notification.name is not None: - registry.notifications[Notification.name] = registry.notifications.get(Notification.name, []) + [Notification] + def __init__(self, name, bases, attrs): + super(MetaNotification, self).__init__(name, bases, attrs) + reg = registry.notifications + if self.name is not None: + reg[self.name] = reg.get(self.name, []) + [self] - sendfn = lambda *args, **kwargs: notify(Notification.name, backends = [Notification], *args, **kwargs) - setattr(Notification, 'send', staticmethod(sendfn)) + sendfn = lambda *args, **kwargs: notify(self.name, backends=[self], *args, **kwargs) + setattr(self, 'send', staticmethod(sendfn)) - return Notification + return -class Notification(object): +class Notification(with_metaclass(MetaNotification, object)): """ Base class for any kind of notifications. Inherit from this class to create - your own notification types and backends. - + your own notification types and backends. + Subclasses need to implement :meth:`notify`. """ - __metaclass__ = MetaNotification name = None """ A name for this notification. """ - + + data = {} + """ + Allow arbitrary data to be stored in the base notification. + """ + def notify(self, *args, **kwargs): """ A method that delivers a notification. """ raise NotImplementedError + def notify(name, *args, **kwargs): """ Send notifications. If ``backends==None``, all backends with the same name - will be used to deliver a notification. - + will be used to deliver a notification. + If ``backends`` is a list, only the specified backends will be used. - + :param name: The notification to send :param backends: A list of backends to be used or ``None`` to use all associated backends """ assert name in registry.notifications, "'{0}' is not a valid notification.".format(repr(name)) - + backends = kwargs.pop('backends', None) - + if backends is None: backends = registry.notifications[name] - + results = [] for Backend in backends: backend = Backend() results.append(backend.notify(*args, **kwargs)) - - return results - + return results diff --git a/yell/backends/celery.py b/yell/backends/celery.py index 10f7fb8..e4eb8ea 100644 --- a/yell/backends/celery.py +++ b/yell/backends/celery.py @@ -1,69 +1,77 @@ from __future__ import absolute_import -from celery.task import Task +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +from future import standard_library +standard_library.install_aliases() +from builtins import * +from .celery.task import Task from yell import Notification, notify, registry + class CeleryNotificationTask(Task): """ Dispatch and run the notification. """ def run(self, name=None, backend=None, *args, **kwargs): """ - The Celery task. - + The Celery task. + Delivers the notification via all backends returned by :param:`backend`. """ assert name is not None, "No 'name' specified to notify" assert backend is not None, "No 'backend' specified to notify with" - + backends = backend().get_backends(*args, **kwargs) notify(name, backends=backends, *args, **kwargs) - + + class CeleryNotification(Notification): - """ - Delivers notifications through Celery. - + """ + Delivers notifications through Celery. + :example: - + :: - + from yell import notify, Notification - + class EmailNotification(Notification): name = 'async' def notify(self, *args, **kwargs): - # Deliver email - + # Deliver email + class DBNotification(Notification): name = 'async' def notify(self, *args, **kwargs): # Save to database - + class AsyncNotification(CeleryNotification): - name = 'async' - + name = 'async' + notify('async', backends = [AsyncNotification], text = "This notification is routed through Celery before being sent and saved") - + In the above example when calling :attr:`yell.notify` will invoke ``EmailNotification`` and ``DBNotification`` once the task was delivered through Celery. - + """ name = None - """ + """ The name of this notification. Override in subclasses. """ - + def get_backends(self, *args, **kwargs): """ Return all backends the task should use to deliver notifications. By default all backends with the same :attr:`name` except for subclasses of :class:`CeleryNotifications` will be used. """ - return filter(lambda cls: not issubclass(cls, self.__class__), registry.notifications[self.name]) + return [ + cls for cls in registry.notifications[self.name] if not issubclass(cls, self.__class__) + ] - def notify(self, *args, **kwargs): - """ + """ Dispatches the notification to Celery """ return CeleryNotificationTask.delay(name=self.name, backend=self.__class__, *args, **kwargs) - - diff --git a/yell/backends/django.py b/yell/backends/django.py index f7de057..83f5d35 100644 --- a/yell/backends/django.py +++ b/yell/backends/django.py @@ -1,12 +1,18 @@ from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + +from future import standard_library +standard_library.install_aliases() +from builtins import * import os -from django.conf import settings -from django import template -from django.core.mail import send_mail, EmailMultiAlternatives +from .django.conf import settings +from .django import template +from .django.core.mail import send_mail, EmailMultiAlternatives from yell import Notification - class EmailBackend(Notification): """ Send emails via :attr:`django.core.mail.send_mail` @@ -16,28 +22,27 @@ class EmailBackend(Notification): """ Email subject """ body = None """ Email body """ - def get_subject(self, *args, **kwargs): """ - Return a subject. Override if you need templating or such. + Return a subject. Override if you need templating or such. """ return self.subject - + def get_body(self, *args, **kwargs): """ Return a body. Override if you need templating or such. """ return self.body - + def get_from(self, *args, **kwargs): return settings.DEFAULT_FROM_EMAIL def get_to(self, *args, **kwargs): return kwargs.get('to') - + def notify(self, *args, **kwargs): - + return send_mail( self.get_subject(*args, **kwargs), self.get_body(*args, **kwargs), @@ -45,61 +50,62 @@ def notify(self, *args, **kwargs): self.get_to(*args, **kwargs), ) + class MultipartEmailBackend(EmailBackend): """ Send emails via :class:`django.core.mail.EmailMultiAlternatives` """ default_content_type = 'text/plain' """ - The default content type. + The default content type. """ - + body = {} """ - Email body mapping content type to message. Requires at least a key for + Email body mapping content type to message. Requires at least a key for :attr:`default_content_type`. """ - def get_body(self, *args, **kwargs): assert self.default_content_type in self.body, "No message body for default content type '%s'" % self.default_content_type return self.body - + def get_default_body(self, *args, **kwargs): return self.get_body(*args, **kwargs)[self.default_content_type] - - def notify(self, *args, **kwargs): + + def notify(self, *args, **kwargs): message = EmailMultiAlternatives( self.get_subject(*args, **kwargs), self.get_default_body(*args, **kwargs), self.get_from(*args, **kwargs), self.get_to(*args, **kwargs) ) - - for content_type, body in self.get_body(*args, **kwargs).iteritems(): + + for content_type, body in list(self.get_body(*args, **kwargs).items()): if content_type == self.default_content_type: continue message.attach_alternative(body, content_type) - + return message.send() - + + class TemplatedEmailBackend(MultipartEmailBackend): """ Generates email bodies from templates. - + :example: - + :: - + class SignupMessage(TemplatedEmailBackend): name = 'signup' subject = "Welcome to %s" % Site.objects.get_current().name content_types = ['text/plain', 'text/html'] - + The `SignupMessage` class would look up following templates for the email body: - + * `yell/signup.txt` * `yell/signup.html` - + """ content_types = ( ('text/plain', '.txt'), @@ -108,10 +114,10 @@ class SignupMessage(TemplatedEmailBackend): """ Default content types to render """ - + # Memoize _body = None - + def get_path(self, name, ext): """ Get the path to a given template name. Override if you wish to @@ -120,7 +126,7 @@ def get_path(self, name, ext): return os.path.join('yell', '{0}{1}'.format(name, ext)) def get_body(self, *args, **kwargs): - """ + """ Render message bodies by using all the content types defined in :attr:`content_types` """ @@ -130,4 +136,3 @@ def get_body(self, *args, **kwargs): tpl = template.loader.get_template(self.get_path(self.name, ext)) self._body[ctype] = tpl.render(template.Context(kwargs)) return self._body - diff --git a/yell/decorators.py b/yell/decorators.py index f6aefd9..3350fe5 100644 --- a/yell/decorators.py +++ b/yell/decorators.py @@ -1,52 +1,61 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals +from future import standard_library +standard_library.install_aliases() +from builtins import * from yell import Notification, notify + class DecoratedNotification(Notification): func = None """ - The function that has been decorated. + The function that has been decorated. """ def notify(self, *args, **kwargs): return self.func(*args, **kwargs) + def notification(name=None, backends=None): """ - Decorator to simplify creation of notification backends. - + Decorator to simplify creation of notification backends. + :example: - + :: - + from yell.decorators import notification - from yell import notify - + from yell import notify + @notification('like') def like_email(user, obj): send_mail("%s liked %s" % (user, obj), "No/Text", "noreply@example.com", [obj.user.email]) - + @notification('like') def like_db(user, obj): DBNotification.objects.create(user = user, obj = obj, type = 'like') - + @notification('like') def like(*args, **kwargs): pass - + # Use all backends - notify('like', user = user, obj = obj) + notify('like', user = user, obj = obj) like.notify(user = user, obj = obj) - + # Only use the email backend like_email.notify_once(user = user, obj = obj) - + """ def wrapper(func): def funcwrapper(self, *args, **kwargs): """ Wrapping the notification function so it doesn't receive `self` """ return func(*args, **kwargs) - + NotificationCls = type('%sNotification' % name.lower().title(), (DecoratedNotification,), { 'func': funcwrapper, 'name': name @@ -54,19 +63,19 @@ def funcwrapper(self, *args, **kwargs): def notify_all(*args, **kwargs): """ - Sends this notification off to every backend with the configured name. + Sends this notification off to every backend with the configured name. """ return notify(name, *args, **kwargs) - + def notify_once(*args, **kwargs): """ Sends this notification off only to the current backend. """ return notify(name, backends=[NotificationCls], *args, **kwargs) - + func.notify = notify_all func.notify_once = notify_once - + return func - + return wrapper diff --git a/yell/registry.py b/yell/registry.py deleted file mode 100644 index 9e33cce..0000000 --- a/yell/registry.py +++ /dev/null @@ -1,2 +0,0 @@ -notifications = {} -""" Registry mapping notification names to backends """ diff --git a/yell/tests.py b/yell/tests.py index 3e503f6..cf6f0d7 100644 --- a/yell/tests.py +++ b/yell/tests.py @@ -1,3 +1,11 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals +from future import standard_library +standard_library.install_aliases() +from builtins import * +from builtins import object from yell import notify, Notification from yell.decorators import notification @@ -36,8 +44,8 @@ def _assert_results(self, results): self.assertTrue('Arg1' == result[1][0], """ Expected value 'Arg1' does not match received value '%s' """ % result[1][0]) self.assertTrue('Arg2' == result[1][1], """ Expected value 'Arg2' does not match received value '%s' """ % result[1][1]) - self.assertTrue('Kwarg1' in result[2].values()) - self.assertTrue('Kwarg2' in result[2].values()) + self.assertTrue('Kwarg1' in list(result[2].values())) + self.assertTrue('Kwarg2' in list(result[2].values())) class TestDecoratorNotification(AssertMixin, unittest.TestCase):