Source code for cnxman.basics

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
.. currentmodule:: cnxman.basics
.. moduleauthor:: Pat Daburu <pat@daburu.net>

This module contains the base classes and basic utilities.
"""

from abc import ABCMeta, abstractmethod
from automat import MethodicalMachine
from enum import Enum
import time
from pydispatch import dispatcher


[docs]class ConnectionException(Exception): """ Raised when an error occurs within a connection. """
[docs] def __init__(self, message: str, inner: Exception): """ :param message: the original message :type message: ``str`` :param inner: the exception responsible for the raising of this exception. :type inner: :py:class:`Exception` """ super().__init__(message) self._inner = inner
@property def inner(self) -> Exception or None: """ This is the original exception responsible for raising this connection exception. """ return self._inner
[docs] @staticmethod def from_exception(ex: Exception): """ This is a convenience method that can be used to create a connection exception from another exception, using default logic to populate the constructor arguments. :param ex: the original exception :type ex: :py:class:`Exception` :return: a new connection exception :rtype: :py:class:`ConnectionException` """ return ConnectionException( message='A(n) {extyp} was raised: {msg}'.format(extyp=type(ex), msg=repr(ex)), inner=ex) # TODO: Improve this!
#@loggable
[docs]class Connection(object): """ Extend this class to define a logical connection to something. The expectations we have of a connection are these: * It can attempt create a connection and report on whether or not the connection was successful. * It can (at least by all appearances) gracefully disconnect. * It can release all its resources upon request. :seealso: :py:func:`Connection.try_connect` :seealso: :py:func:`Connection.disconnect` :seealso: :py:func:`Connection.teardown` """ __metaclass__ = ABCMeta
[docs] class Signals(Enum): """ These are the used by connection objects. :seealso: :py:func:`pydispatch.dispatcher` """ RAISE_ALARM = 'raise-alarm' # Something has gone awry with the connection.
[docs] @abstractmethod def try_connect(self) -> bool: """ Override this method to define the logic by which a connection is make. :return: ``True`` if and only if the connection attempt is successful, otherwise ``False``. :rtype: ``bool`` """ pass
[docs] @abstractmethod def disconnect(self): """ Override this method to take the steps required to gracefully disconnect. """ pass
[docs] @abstractmethod def teardown(self): """ Override this method to release resources when requested. """ pass
[docs] def raise_alarm(self): """ Raise the alarm to notify anyone who might be interested (like a :py:class:`ConnectionManager`) that there is trouble with the connection. """ dispatcher.send(signal=Connection.Signals.RAISE_ALARM, sender = self)
[docs]class ConnectionManager(object): """ Extend this class to create your own object with the know-how to establish and maintain a connection to something. """ __metaclass__ = ABCMeta _machine = MethodicalMachine() # This is the class state machine.
[docs] def __init__(self, connection: Connection): self._connection = connection # We want to be notified if the connection raises the alarm. dispatcher.connect(self._handle_connection_raise_alarm, signal=Connection.Signals.RAISE_ALARM, sender=self._connection)
@_machine.state(initial=True) def ready(self): """We haven't connected yet, but we're ready to try.""" @_machine.input() def connect(self): """Let's get connected.""" @_machine.output() def _connect(self): """ This is the output method mapped to the :py:func:`ConnectionManager.connect` input method. """ # If there is no function to call, we simply cannot connect, so... if self._connection is None: self._raise_alarm() return else: # Give it a try! connected = self._connection.try_connect() # How'd it go? if connected: self._silence_alarm() # Great! else: self._raise_alarm() # OK. Not so great. @_machine.input() def _raise_alarm(self): """ There is trouble with the connection. Raise the alarm! """ @_machine.input() def _silence_alarm(self): """ Everything is fine with the connection. """ def _handle_connection_raise_alarm(self): """ This is a handler for the connection's 'raise alarm' signal. :seealso: :py:class:`Connection.Signals` """ self._raise_alarm() @_machine.state() def connecting(self): """We're trying to connect.""" @_machine.state() def connected(self): """We're connected.""" @_machine.state() def recovering(self): """We're waiting to try to reconnect.""" # TODO: Improve the _recover method! @_machine.output() def _recover(self): """ Attempt to recover the connection. """ print("Waiting to retry.") time.sleep(5) print("Here we go.") self.connect() @_machine.state() def disconnected(self): """The connection has been disconnected.""" @_machine.input() def disconnect(self): """ Release the connection. """ @_machine.output() def _disconnect(self): """ This is the output method mapped to the :py:func:`ConnectionManager.disconnect` input method. """ self._connection.disconnect() @_machine.state(terminal=True) def torndown(self): """The connection manager has been torn down. It's over.""" @_machine.input() def teardown(self): """ Release any resources held by the connection. """ @_machine.output() def _teardown(self): """ This is the output method mapped to the :py:func:`ConnectionManager.teardown` input method. """ self._connection.teardown() # From the 'ready' state, we can connect. ready.upon(connect, enter=connecting, outputs=[_connect]) # From the 'connecting' state, we can either go into an "everything's OK" state by silencing any alarms... connecting.upon(_silence_alarm, enter=connected, outputs=[]) # ...or we can raise the alarm. connecting.upon(_raise_alarm, enter=recovering, outputs=[_recover]) # From the 'recovering' state, we can try to connect. recovering.upon(connect, enter=connecting, outputs=[_connect]) # If we're recovering, we don't need to change state if the alarm sounds because we're already in a recovery # condition. recovering.upon(_raise_alarm, enter=recovering, outputs=[]) # When we're connected, we can, of course, go to the 'disconnected' state. connected.upon(disconnect, enter=disconnected, outputs=[_disconnect]) # When we're connected, we can go right to the 'torndown' state if requested. connected.upon(teardown, enter=torndown, outputs=[_disconnect, _teardown]) # When we're in the 'connected' state, raising an alarm puts us into the 'recovering' state. connected.upon(_raise_alarm, enter=recovering, outputs=[_recover]) # When we're in the 'connected' state, silencing an alarm puts us into the 'connected' state. connected.upon(_silence_alarm, enter=connected, outputs=[]) # From the 'disconnected' state, we can to the 'torndown' state. disconnected.upon(teardown, enter=torndown, outputs=[_teardown])