Source code for m4us.backplanes

# -*- coding: utf-8 -*-

#---Header---------------------------------------------------------------------

# This file is part of Message For You Sir (m4us).
# Copyright © 2009-2012 Krys Lawrence
#
# Message For You Sir is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Message For You Sir is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Message For You Sir.  If not, see <http://www.gnu.org/licenses/>.


"""Provides `backplanes` for `publisher`/`subscriber` interactions."""


#---Imports--------------------------------------------------------------------

#---  Standard library imports
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
## pylint: disable=W0622, W0611
from future_builtins import ascii, filter, hex, map, oct, zip  ## NOQA
## pylint: enable=W0622, W0611

#---  Third-party imports
## pylint: disable=F0401
from zope import interface
## pylint: enable=F0401

#---  Project imports
from m4us.core import api as core
from m4us import interfaces


#---Globals--------------------------------------------------------------------


#---Functions------------------------------------------------------------------

@core.coroutine()
@interface.implementer(interfaces.IBackplane)
@interface.provider(interfaces.IBackplaneFactory)
[docs]def backplane(): """A `backplane` lets `publishers` send `messages` to `subscribers`. To create a `backplane`, just call this function and store the resulting `backplane` `coroutine`. Then make the `backplane` available for import by any publishing and subscribing modules. To publish `messages` to the `backplane`, use the :func:`publish_to` function. To subscribe to a `backplane`, just `link` to the `backplane`'s ``outbox`` and ``signal`` `outboxes` like any other `publisher` `coroutine`. :implements: :class:`m4us.interfaces.IBackplane` :provides: :class:`m4us.interfaces.IBackplaneFactory` .. seealso:: The :class:`~m4us.interfaces.IBackplane` interface for more details on `backplanes`. """ publishers = set() inbox, message = (yield) while True: response = 'outbox', message if core.is_shutdown(inbox, message): if not publishers: yield 'signal', message break response = None elif inbox == 'control': response = 'signal', message registration = interfaces.IRegisterPublisher(message, None) if registration: publisher = registration.publisher if publisher in publishers: ## pylint: disable=W0710 raise AlreadyRegisteredError(publisher=publisher) ## pylint: enable=W0710 publishers.add(publisher) response = None else: unregistration = interfaces.IUnregisterPublisher(message, None) if unregistration: publisher = unregistration.publisher try: publishers.remove(publisher) except KeyError: ## pylint: disable=W0710 raise NotRegisteredError(publisher=publisher) ## pylint: enable=W0710 response = None inbox, message = (yield response)
[docs]def publish_to(backplane_): """Return a `component` that can publish `messages` to the `backplane`. This is just a convenience function that returns a :class:`~m4us.core.containers.Pipeline` of a :class:`Publisher` `component`, the given :func:`backplane` object and a :func:`~m4us.core.coroutines.null_sink` `coroutine`. :param m4us.interfaces.IBackplane backplane: The `backplane` object to which to publish `messages`. :returns: A container `component` that will publish all sent `messages` to the given `backplane`. :rtype: :class:`m4us.core.containers.Pipeline` .. note:: Since this function returns an :class:`~m4us.core.interfaces.IContainer` `component`, remember to register it's `coroutines` and `links` with the `scheduler` and `post office`, or include it in another container. """ return core.Pipeline(Publisher(), backplane_, core.null_sink()) #---Classes-------------------------------------------------------------------- #--- Exceptions
[docs]class AlreadyRegisteredError(core.M4USException): """Raised when a `publisher` has attempted to register more than once. The default message requires a ``publisher`` attribute or keyword argument. .. seealso:: The :class:`~m4us.interfaces.IBackplane` `interface` for details about this exeption's usage. """ _message = 'The publisher "{publisher!r}" was already registered.'
[docs]class NotRegisteredError(core.M4USException): """Raised when a `publisher` has not been registered by a `backplane`. The default message requires a ``publisher`` attribute or keyword argument. .. seealso:: The :class:`~m4us.interfaces.IBackplane` `interface` for details about this exeption's usage. """ _message = 'The publisher "{publisher!r}" was not previsously registered.' #--- Messages
@interface.implementer(interfaces.IRegisterPublisher) @interface.provider(core.IMessageFactory)
[docs]class RegisterPublisher(core.Message): """`Message` for registering `publishers` with `backplanes`. :param publisher: The publishing object, specified as a keyword argument. :implements: :class:`m4us.interfaces.IRegisterPublisher` :provides: :class:`m4us.core.interfaces.IMessageFactory` .. seealso:: The :class:`Publisher` `component` and the :class:`~m4us.interfaces.IBackplane` `interface` for details about how this `message` is used. .. attribute:: publisher The `publisher` object to register. """ ## pylint: disable=R0903 def __init__(self, **kwargs): """See class docstring for this method's documentation.""" # Note: The publisher argument should always be spcified as a keyword # argument in order to conform the to IMessageFactory interface. assert b'publisher' in kwargs, '"publisher" keyword argument required.' core.Message.__init__(self, **kwargs) ## pylint: disable=R0903
@interface.implementer_only(interfaces.IUnregisterPublisher) @interface.provider(core.IMessageFactory)
[docs]class UnregisterPublisher(RegisterPublisher): """`Message` for unregistering `publishers` with `backplanes`. :param publisher: The publishing object, specified as a keyword argument. :implements: :class:`m4us.interfaces.IUnregisterPublisher` :provides: :class:`m4us.core.interfaces.IMessageFactory` .. seealso:: The :class:`Publisher` `component` and the :class:`~m4us.interfaces.IBackplane` `interface` for details about how this `message` is used. .. attribute:: publisher The `publisher` object to unregister. """ ## pylint: enable=R0903 #--- Components # This is needed so that all shutdown messages are emitted after the IShutdown # is sent in.
@interface.implementer(core.INotLazy) @interface.provider(core.ICoroutineFactory)
[docs]class Publisher(core.Component): """`Component` that registers with a `backplane` before sending `messages`. :class:`~m4us.interfaces.IBackplane` `coroutines` expect that registration and un-registration `messages` will be sent by `publishers` before and after usage. This `component` handles those actions transparently. Just link this `component`'s ``outbox`` and ``signal`` `outboxes` to the `backplane`'s ``inbox`` and ``control`` `inboxes`. This is usually all handled automatically by the :func:`publish_to` function. `Messages` sent to this class will be forwarded back out, but only after an :class:`~m4us.interfaces.IRegisterPublisher` `message` is emitted first. Similarily, before an :class:`~m4us.core.interfaces.IShutdown` `message` is forwarded on, an :class:`~m4us.interfaces.IUnregisterPublisher` `message` is emitted first. :implements: :class:`m4us.core.interfaces.ICoroutine` and :class:`m4us.core.interfaces.INotLazy` :provides: :class:`m4us.core.interfaces.ICoroutineFactory` .. seealso:: The :func:`publish_to` function for the normal way to publish to a `backplane`. .. seealso:: The :class:`RegisterPublisher` and :class:`UnregisterPublisher` classes as examples of concrete classes that `publishers` can use for registration and unregistration. """ ## pylint: disable=R0903 def _main(self): """Publish `messages` to a `backplane`. .. seealso:: This class' docstring and the :meth:`~m4us.core.interfaces.IComponent._main` method for details about this method. """ current_inbox, current_message = (yield) # The use of self here is why this is a component and not just a # coroutine. response = 'signal', RegisterPublisher(publisher=self) while True: next_inbox, next_message = (yield response) if current_inbox == 'control' and current_message is None: response = None elif core.is_shutdown(current_inbox, current_message): yield 'signal', UnregisterPublisher(publisher=self) yield 'signal', current_message break else: response = 'outbox', current_message current_inbox, current_message = next_inbox, next_message #---Module initialization------------------------------------------------------ #---Late Imports--------------------------------------------------------------- #---Late Globals--------------------------------------------------------------- #---Late Functions------------------------------------------------------------- #---Late Classes--------------------------------------------------------------- #---Late Module initialization-------------------------------------------------