Skip to content

Commit

Permalink
Merge ae09792 into e77ce37
Browse files Browse the repository at this point in the history
  • Loading branch information
matllubos committed Sep 26, 2019
2 parents e77ce37 + ae09792 commit f2b0199
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 158 deletions.
17 changes: 7 additions & 10 deletions chamber/models/dispatchers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import types

from collections import defaultdict

from django.core.exceptions import ImproperlyConfigured

from chamber.utils.transaction import (
on_success, OnSuccessHandler, OneTimeOnSuccessHandler, InstanceOneTimeOnSuccessHandler
)

from .signals import dispatcher_post_save
from .handlers import BaseHandler


class BaseDispatcher:
Expand All @@ -17,13 +15,12 @@ class BaseDispatcher:

signal = None

def _validate_init_params(self):
if not callable(self.handler):
raise ImproperlyConfigured('Registered handler must be a callable.')

def __init__(self, handler, signal=None):
self.handler = handler
self._validate_init_params()
assert (isinstance(handler, types.FunctionType)
or isinstance(handler, BaseHandler)), 'Handler must be function or instance of ' \
'chamber.models.handlers.BaseHandler, {}: {}'.format(self,
handler)
self._connected = defaultdict(list)
self._signal = signal if signal is not None else self.signal

Expand Down
85 changes: 85 additions & 0 deletions chamber/models/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from django.core.exceptions import ImproperlyConfigured

from chamber.utils.transaction import on_success, UniqueOnSuccessCallable
from chamber.models.signals import dispatcher_post_save


class BaseHandler:
"""
Base dispatcher class that can be subclassed to call a handler based on a change in some a SmartModel.
If you subclass, be sure the __call__ method does not change signature.
"""

signal = None

def __init__(self, signal=None):
self._signal = signal if signal is not None else self.signal

def connect(self, sender):
if not self._signal:
raise ImproperlyConfigured('Handler cannot be connected as dispatcher, because signal is not set')
self._signal.connect(self, sender=sender)

def __call__(self, instance, **kwargs):
"""
`instance` ... instance of the SmartModel where the handler is being called
Some dispatchers require additional params to evaluate the handler can be dispatched,
these are hidden in args and kwargs.
"""
if self.can_handle(instance, **kwargs):
self._handle(instance, **kwargs)

def _handle(self, instance, **kwargs):
self.handle(instance, **kwargs)

def can_handle(self, instance, **kwargs):
return True

def handle(self, instance, **kwargs):
raise NotImplementedError


class OnSuccessHandler(BaseHandler):
"""
Handler class that is used for performing on success operations.
"""

signal = dispatcher_post_save

def __init__(self, using=None, *args, **kwargs):
self.using = using
super().__init__(*args, **kwargs)

def _handle(self, instance, **kwargs):
on_success(lambda: self.handle(instance, **kwargs), using=self.using)


class InstanceOneTimeOnSuccessHandlerCallable(UniqueOnSuccessCallable):
"""
Use this class to create on success caller that will be unique per instance and will be called only once per
instance.
"""

def __init__(self, handler, instance):
super().__init__(instance=instance)
self.handler = handler

def _get_instance(self):
instance = self.kwargs_list[0]['instance']
return instance.__class__.objects.get(pk=instance.pk)

def _get_unique_id(self):
instance = self.kwargs_list[0]['instance']
return hash((self.handler.__class__, instance.__class__, instance.pk))

def __call__(self):
self.handler.handle(instance=self._get_instance())


class InstanceOneTimeOnSuccessHandler(OnSuccessHandler):
"""
Use this class to create handler that will be unique per instance and will be called only once per instance.
"""

def _handle(self, instance, **kwargs):
on_success(InstanceOneTimeOnSuccessHandlerCallable(self, instance), using=self.using)
108 changes: 36 additions & 72 deletions chamber/utils/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,39 +25,39 @@ def atomic(func):

class TransactionSignalsContext:
"""
Context object that stores handlers and call it after successful pass trough surrounded code block
with "transaction_signals decorator. Handlers can be unique or standard. Unique handlers are registered
Context object that stores callable and call it after successful pass trough surrounded code block
with "transaction_signals decorator. Handlers can be unique or standard. Unique callable are registered
and executed only once.
"""

def __init__(self):
self._unique_handlers = OrderedDict()
self._handlers = []
self._unique_callable = OrderedDict()
self._callable_list = []

def register(self, handler):
if getattr(handler, 'is_unique', False):
if hash(handler) in self._unique_handlers:
self._unique_handlers.get(hash(handler)).join(handler)
def register(self, callable):
if isinstance(callable, UniqueOnSuccessCallable):
if hash(callable) in self._unique_callable:
self._unique_callable.get(hash(callable)).join(callable)
else:
self._unique_handlers[hash(handler)] = handler
self._handlers.append(handler)
self._unique_callable[hash(callable)] = callable
self._callable_list.append(callable)
else:
self._handlers.append(handler)
self._callable_list.append(callable)

def handle_all(self):
for handler in self._handlers:
handler()
for callable in self._callable_list:
callable()

def join(self, transaction_signals_context):
for handler in transaction_signals_context._handlers:
self.register(handler)
for callable in transaction_signals_context._callable_list:
self.register(callable)


class TransactionSignals(ContextDecorator):
"""
Context decorator that supports usage python keyword "with".
Decorator that adds transaction context to the connection on input.
Finally handlers are called on the output.
Finally callables are called on the output.
"""

def __init__(self, using):
Expand All @@ -81,30 +81,30 @@ def __exit__(self, exc_type, exc_value, traceback):
connection.transaction_signals_context_list[-1].join(transaction_signals_context)


def on_success(handler, using=None):
def on_success(callable, using=None):
"""
Register a handler or a function to be called after successful code pass.
If transaction signals are not active the handler/function is called immediately.
:param handler: handler or function that will be called.
Register a callable or a function to be called after successful code pass.
If transaction signals are not active the callable/function is called immediately.
:param callable: callable or function that will be called.
:param using: name of the database
"""

connection = get_connection(using)
if getattr(connection, 'transaction_signals_context_list', False):
connection.transaction_signals_context_list[-1].register(handler)
connection.transaction_signals_context_list[-1].register(callable)
else:
if settings.DEBUG:
logger.warning(
'For on success signal should be activated transaction signals via transaction_signals decorator.'
'Function is called immediately now.'
)
handler()
callable()


def transaction_signals(using=None):
"""
Decorator that adds transaction context to the connection on input.
Finally handlers are called on the output.
Finally callable are called on the output.
:param using: name of the database
"""
if callable(using):
Expand All @@ -125,72 +125,36 @@ def atomic_with_signals(func):
return transaction.atomic(transaction_signals(func))


class OnSuccessHandler:
class UniqueOnSuccessCallable:
"""
Handler class that is used for performing on success operations.
One time callable class that is used for performing on success operations.
Handler is callable only once, but data of all calls are stored inside list (kwargs_list).
"""

is_unique = False

def __init__(self, using=None, **kwargs):
self.kwargs = kwargs
on_success(self, using=using)

def __call__(self):
self.handle(**self.kwargs)

def handle(self, **kwargs):
"""
There should be implemented handler operations.
:param kwargs: input data that was send during hanlder creation.
"""
raise NotImplementedError


class OneTimeOnSuccessHandler(OnSuccessHandler):
"""
One time handler class that is used for performing on success operations.
Handler is called only once, but data of all calls are stored inside list (kwargs_list).
"""

is_unique = True

def __init__(self, using=None, **kwargs):
def __init__(self, **kwargs):
self.kwargs_list = (kwargs,)
on_success(self, using=using)

def join(self, handler):
def join(self, callable):
"""
Joins two unique handlers.
Joins two unique callable.
"""
self.kwargs_list += handler.kwargs_list
self.kwargs_list += callable.kwargs_list

def _get_unique_id(self):
"""
Unique handler must be identified with some was
Unique callable must be identified with some was
:return:
"""
return None

def __hash__(self):
return hash((self.__class__, self._get_unique_id()))

def _get_kwargs(self):
return self.kwargs_list[-1]

def __call__(self):
self.handle(self.kwargs_list)
self.handle()

def handle(self, kwargs_list):
def handle(self):
raise NotImplementedError


class InstanceOneTimeOnSuccessHandler(OneTimeOnSuccessHandler):
"""
Use this class to create handler that will be unique per instance and will be called only once per instance.
"""

def _get_instance(self):
instance = self.kwargs_list[0]['instance']
return instance.__class__.objects.get(pk=instance.pk)

def _get_unique_id(self):
instance = self.kwargs_list[0]['instance']
return hash((instance.__class__, instance.pk))
68 changes: 66 additions & 2 deletions docs/dispatchers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ When the handler is fired, it is passed a single argument -- the instance of the

::

def send_email(user):
def send_email(user, **kwargs):
# Code that actually sends the e-mail
send_html_email(recipient=user.email, subject='Your profile was updated')

Expand Down Expand Up @@ -70,7 +70,7 @@ be dispatched during the ``_pre_save`` method when the state changes to

.. code:: python
def my_handler(my_smart_model): # Do that useful stuff
def my_handler(instance, **kwargs): # Do that useful stuff
pass
class MySmartModel(chamber\_models.SmartModel):
Expand All @@ -84,3 +84,67 @@ be dispatched during the ``_pre_save`` method when the state changes to
dispatchers = (
StateDispatcher(my_handler, STATE, state, STATE.SECOND, signal=dispatcher_pre_save),
)
Model Handlers
==============
Dispatchers can be used with function but for more complex situations instance
of ``chamber.models.handlers.BaseHandler`` can be used instead of function.
.. code:: python
class MyHandler(BaseHandler):
def handle(self, instance, **kwargs): # Do that useful stuff
pass
class MySmartModel(chamber\_models.SmartModel):
STATE = ChoicesNumEnum(
('FIRST', _('first'), 1),
('SECOND', _('second'), 2),
)
state = models.IntegerField(choices=STATE.choices, default=STATE.FIRST)
dispatchers = (
StateDispatcher(MyHandler(), STATE, state, STATE.SECOND, signal=dispatcher_pre_save),
)
Moreover handler can also serve as a dispatcher.
class MyHandler(BaseHandler):
def handle(self, instance, **kwargs): # Do that useful stuff
pass
def can_handle(self, instance, **kwargs):
return ... # Define if handler will be called
class MySmartModel(chamber\_models.SmartModel):
STATE = ChoicesNumEnum(
('FIRST', _('first'), 1),
('SECOND', _('second'), 2),
)
state = models.IntegerField(choices=STATE.choices, default=STATE.FIRST)
dispatchers = (
MyHandler(signal=dispatcher_pre_save),
)
There are two special types of handlers ``chamber.models.handlers.OnSuccessHandler``
and ``chamber.models.handlers.InstanceOneTimeOnSuccessHandler``.
.. class:: chamber.models.dispatchers.OnSuccessHandler
The handler uses ``chamber.utils.transaction.on_success`` to handle itself only if transaction is successful.
In the most cases the handler will be used with ``dispatcher_post_save`` signal therefore ``dispatcher_post_save``
is default.
.. class:: chamber.models.dispatchers.InstanceOneTimeOnSuccessHandler
Descendant of the ``hamber.models.dispatchers.OnSuccessHandler`` with the difference that is called only one
per model instance.
WARNING: Be carefull using ``chamber.models.handlers.OnSuccessHandler``and
``chamber.models.handlers.InstanceOneTimeOnSuccessHandler``. Handlers should not invoke another handlers or code which
uses ``chamber.utils.transaction.on_success`` because the code will not be invoked.

0 comments on commit f2b0199

Please sign in to comment.