Signals
=======
CKAN provides built-in signal support, powered by `blinker
`_.
The same library is used by `Flask
`_ and anything
written in the Flask documentation also applies to CKAN. Probably, the most
important point:
Flask comes with a couple of signals and other extensions
might provide more. Also keep in mind that signals are intended to
notify subscribers and should not encourage subscribers to modify
data. You will notice that there are signals that appear to do the
same thing like some of the builtin decorators do (eg:
request_started is very similar to before_request()). However,
there are differences in how they work. The core before_request()
handler, for example, is executed in a specific order and is able
to abort the request early by returning a response. In contrast
all signal handlers are executed in undefined order and do not
modify any data.
:mod:`ckan.lib.signals` provides two namespaces for signals: ``ckan``
and ``ckanext``. All core signals reside in ``ckan``, while signals
from extensions (``datastore``, ``datapusher``, third-party
extensions) are registered under ``ckanext``. This is a recommended pattern
and nothing prevents developers from creating and using
their own namespaces.
Signal subscribers **MUST** always be defined as callable accepting
one mandatory argument `sender` and arbitary number of keyword
arguments::
def subscriber(sender, **kwargs):
...
CKAN core doesn't make any guarantees as for the concrete named arguments
that will be passed to subscriber. For particular CKAN version one can
use signlal-listing below as a reference, but in future versions
signature may change. In additon, any event can be fired by
a third-party plugin, so it is always safer to check whether a particular
argument is available inisde the provided `kwargs`.
Even though it is possible to register subscribers using decorators::
@p.toolkit.signals.before_action.connect
def action_subscriber(sender, **kwargs):
pass
the recommended approach is to use the
:class:`ckan.plugins.interfaces.ISignal` interface, in order to give CKAN more
control over the subscriptions available depending on the enabled plugins::
class ExampleISignalPlugin(p.SingletonPlugin):
p.implements(p.ISignal)
def get_signal_subscriptions(self):
return {
p.toolkit.signals.before_action: [
# when subscribing to every signal of type
action_subscriber,
# when subscribing to signals from particular sender
{u'receiver': action_subscriber, u'sender': 'sender_name'}
]
}
.. warning:: Arguments passed to subscribers should never be
modified. Use subscribers only to trigger side effects and
not to change existing CKAN behavior. If one needs to alter
CKAN behavior use :mod:`ckan.plugins.interfaces` instead.
There are a number of built-in signals in CKAN (check the list at the bottom
of the page). All of them are created inside one of the
available namespaces: ``ckan`` and ``ckanext``. For simplicity sake,
all built in signals have aliases inside ``ckan.lib.signals`` (or
``ckan.plugins.toolkit.signals``, or ``ckantoolkit.signals``), but you
can always get signals directly from corresponding the namespace
(you shouldn't use this directly unless you are familiar with the ``blinker``
library)::
from ckan.lib.signals import (
ckan as ckan_namespace,
register_blueprint, request_started
)
assert register_blueprint is ckan_namespace.signal('register_blueprint')
assert request_started is ckan_namespace.signal('request_started')
This information may be quite handy, if you want to define custom
signals inside your extension. Just use ``ckanext`` namespace and call
its method ``signal`` in order to create a new signal (or get an existing one).
In order to avoid name collisions and unexpected behavior,
always use your plugin's name as prefix for the signal.::
# ckanext-custom/ckanext/custom/signals.py
import ckan.plugins.toolkit as tk
# create signal and use it somewhere inside your extension
custom_something_happened = tk.signals.ckanext.signal('custom_something_happened)
# after this, you can notify subscribers using following code:
custom_signal_happened.send(SENDER, ARG1=VALUE1, ARG2=VALUE2, ...)
From now on, everyone who is using your extension can subscribe to your
signal from another extension::
# ckanext-ext/ckanext/ext/plugin.py
import ckan.plugins as p
from ckanext.custom.signals import custom_something_happened
from ckanext.ext import listeners # here you'll define listeners
class ExtPlugin(p.SingletonPlugin):
p.implements(p.ISignal)
def get_signal_subscriptions(self):
return {
custom_something_happened: [
listeners.custom_listener
]
}
There is a small problem in snippet above. If ``ckanext-custom`` is
not installed, you'll get ``ImportError``. This is perfectly fine if
you are sure that you are using ``ckanext-custom``, but may be a
problem for some general-use plugin. To avoid this, import signals from
the ``ckanext`` namespace instead::
# ckanext-ext/ckanext/ext/plugin.py
import ckan.plugins as p
from ckanext.ext import listeners
class ExtPlugin(p.SingletonPlugin):
p.implements(p.ISignal)
def get_signal_subscriptions(self):
custom_something_happened = p.toolkit.signals.ckanext.signal(
'custom_something_happened'
)
return {
custom_something_happened: [
listeners.custom_listener
]
}
All signals are singletons inside their namespace. If ``ckanext-custom``
is installed, you'll get its existing signal, otherwise you'll create a new
signal that is never sent. So your subscription will work only
when ``ckanext-custom`` is available and do nothing otherwise.
:py:mod:`ckan.lib.signals` contains a few core signals for
plugins to subscribe:
.. currentmodule:: ckan.lib.signals
.. autodata:: request_started
:annotation: (app)
.. autodata:: request_finished
:annotation: (app, response)
.. autodata:: register_blueprint
:annotation: (blueprint_type, blueprint)
.. autodata:: resource_download
:annotation: (resource_id)
.. autodata:: failed_login
:annotation: (username)
.. autodata:: user_created
:annotation: (username, user)
.. autodata:: request_password_reset
:annotation: (username, user)
.. autodata:: perform_password_reset
:annotation: (username, user)
.. autodata:: action_succeeded
:annotation: (action, context, data_dict, result)
.. autodata:: datastore_upsert
:annotation: (resource_id, records)
.. autodata:: datastore_delete
:annotation: (resource_id, result, data_dict)