From 0d12ca5de07ae312acc0f405de42895e2258d6a6 Mon Sep 17 00:00:00 2001 From: Andy Beaumont Date: Sun, 21 Jun 2015 22:52:20 +0100 Subject: [PATCH 1/5] Allow arbitrary data to be stored in the base notification. --- yell/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yell/__init__.py b/yell/__init__.py index 86c1035..14de528 100644 --- a/yell/__init__.py +++ b/yell/__init__.py @@ -32,6 +32,12 @@ class Notification(object): 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. From ddb1d8153f7be7e56a5e21adb642882bafb55d67 Mon Sep 17 00:00:00 2001 From: Andy Beaumont Date: Sun, 3 Sep 2017 23:15:28 +0100 Subject: [PATCH 2/5] Attempting Python 3 compatibility --- yell/__init__.py | 3 ++- yell/backends/celery.py | 8 ++++++-- yell/backends/django.py | 12 ++++++++---- yell/decorators.py | 4 ++++ yell/registry.py | 16 ++++++++++++++-- yell/tests.py | 8 ++++++-- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/yell/__init__.py b/yell/__init__.py index 14de528..6089a57 100644 --- a/yell/__init__.py +++ b/yell/__init__.py @@ -1,6 +1,7 @@ __version__ = "0.3.2" -import registry +from .registry import registry + class MetaNotification(type): """ diff --git a/yell/backends/celery.py b/yell/backends/celery.py index 10f7fb8..7197f3a 100644 --- a/yell/backends/celery.py +++ b/yell/backends/celery.py @@ -1,5 +1,9 @@ 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 .celery.task import Task from yell import Notification, notify, registry class CeleryNotificationTask(Task): @@ -57,7 +61,7 @@ def get_backends(self, *args, **kwargs): 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): diff --git a/yell/backends/django.py b/yell/backends/django.py index f7de057..3388578 100644 --- a/yell/backends/django.py +++ b/yell/backends/django.py @@ -1,8 +1,12 @@ from __future__ import absolute_import +from __future__ import print_function +from __future__ import division +from __future__ import unicode_literals + 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 @@ -76,7 +80,7 @@ def notify(self, *args, **kwargs): self.get_to(*args, **kwargs) ) - for content_type, body in self.get_body(*args, **kwargs).iteritems(): + for content_type, body in self.get_body(*args, **kwargs).items(): if content_type == self.default_content_type: continue message.attach_alternative(body, content_type) diff --git a/yell/decorators.py b/yell/decorators.py index f6aefd9..18a0cf1 100644 --- a/yell/decorators.py +++ b/yell/decorators.py @@ -1,3 +1,7 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals from yell import Notification, notify class DecoratedNotification(Notification): diff --git a/yell/registry.py b/yell/registry.py index 9e33cce..f217a03 100644 --- a/yell/registry.py +++ b/yell/registry.py @@ -1,2 +1,14 @@ -notifications = {} -""" Registry mapping notification names to backends """ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals + + +class Reg(object): + """ Registry mapping notification names to backends """ + + def __init__(self): + self.notifications = {} + + +registry = Reg() diff --git a/yell/tests.py b/yell/tests.py index 3e503f6..ea6eb73 100644 --- a/yell/tests.py +++ b/yell/tests.py @@ -1,3 +1,7 @@ +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from __future__ import unicode_literals from yell import notify, Notification from yell.decorators import notification @@ -36,8 +40,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): From 6824d4ef45273a87c730a5333e12a548784f353e Mon Sep 17 00:00:00 2001 From: Andy Beaumont Date: Mon, 4 Sep 2017 14:13:41 +0100 Subject: [PATCH 3/5] Lots of PEP8 tidying and updated the metaclass to work with Python 3 --- yell/__init__.py | 84 +++++++++++++++++++++++++++++------------ yell/backends/celery.py | 52 +++++++++++++------------ yell/backends/django.py | 57 ++++++++++++++-------------- yell/decorators.py | 43 +++++++++++---------- yell/registry.py | 14 ------- yell/tests.py | 4 ++ 6 files changed, 144 insertions(+), 110 deletions(-) diff --git a/yell/__init__.py b/yell/__init__.py index 6089a57..82caf68 100644 --- a/yell/__init__.py +++ b/yell/__init__.py @@ -1,73 +1,107 @@ +# -*- 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.3.2" -from .registry import registry + +import hues + + +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() 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) + def __init__(self, name, bases, attrs): + super(MetaNotification, self).__init__(name, bases, attrs) + reg = registry.notifications + hues.info("META NOTIFICATION") + hues.info(self) + if self.name is not None: + reg[self.name] = reg.get(self.name, []) + [self] - if Notification.name is not None: - registry.notifications[Notification.name] = registry.notifications.get(Notification.name, []) + [Notification] + sendfn = lambda *args, **kwargs: notify(self.name, backends=[self], *args, **kwargs) + setattr(self, 'send', staticmethod(sendfn)) - sendfn = lambda *args, **kwargs: notify(Notification.name, backends = [Notification], *args, **kwargs) - setattr(Notification, 'send', staticmethod(sendfn)) + return - return Notification - -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 7197f3a..e4eb8ea 100644 --- a/yell/backends/celery.py +++ b/yell/backends/celery.py @@ -3,71 +3,75 @@ 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 [cls for cls in registry.notifications[self.name] if not issubclass(cls, self.__class__)] + 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 3388578..83f5d35 100644 --- a/yell/backends/django.py +++ b/yell/backends/django.py @@ -3,6 +3,9 @@ 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 @@ -10,7 +13,6 @@ from yell import Notification - class EmailBackend(Notification): """ Send emails via :attr:`django.core.mail.send_mail` @@ -20,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), @@ -49,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).items(): + + 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'), @@ -112,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 @@ -124,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` """ @@ -134,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 18a0cf1..3350fe5 100644 --- a/yell/decorators.py +++ b/yell/decorators.py @@ -2,55 +2,60 @@ 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 @@ -58,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 index f217a03..e69de29 100644 --- a/yell/registry.py +++ b/yell/registry.py @@ -1,14 +0,0 @@ -from __future__ import print_function -from __future__ import division -from __future__ import absolute_import -from __future__ import unicode_literals - - -class Reg(object): - """ Registry mapping notification names to backends """ - - def __init__(self): - self.notifications = {} - - -registry = Reg() diff --git a/yell/tests.py b/yell/tests.py index ea6eb73..cf6f0d7 100644 --- a/yell/tests.py +++ b/yell/tests.py @@ -2,6 +2,10 @@ 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 From 0d04d790da769c606e356b435e6e2cf400c28be1 Mon Sep 17 00:00:00 2001 From: Andy Beaumont Date: Fri, 8 Sep 2017 21:43:01 +0100 Subject: [PATCH 4/5] Removed some logging and bumber the version number. --- yell/__init__.py | 7 +------ yell/registry.py | 0 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 yell/registry.py diff --git a/yell/__init__.py b/yell/__init__.py index 82caf68..1095ff3 100644 --- a/yell/__init__.py +++ b/yell/__init__.py @@ -18,10 +18,7 @@ from builtins import object from future.utils import with_metaclass -__version__ = "0.3.2" - - -import hues +__version__ = "0.4.0" class Reg(object): @@ -45,8 +42,6 @@ class MetaNotification(type): def __init__(self, name, bases, attrs): super(MetaNotification, self).__init__(name, bases, attrs) reg = registry.notifications - hues.info("META NOTIFICATION") - hues.info(self) if self.name is not None: reg[self.name] = reg.get(self.name, []) + [self] diff --git a/yell/registry.py b/yell/registry.py deleted file mode 100644 index e69de29..0000000 From ad6437c614171fa245e5ab029ac46826fd8c1ffd Mon Sep 17 00:00:00 2001 From: Andy Beaumont Date: Tue, 10 Sep 2019 21:57:49 +0100 Subject: [PATCH 5/5] Adds a version number as a string to setup.py so that it can be installed with Poetry. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.',