import abc
import datetime
from multiprocessing import cpu_count

from statscache.plugins.schedule import Schedule
from statscache.plugins.models import BaseModel, ScalarModel,\
                                      CategorizedModel, CategorizedLogModel,\
                                      ConstrainedCategorizedLogModel
from statscache.plugins.threads import Queue, Future, asynchronous


class BasePlugin(object):
    """ An abstract base class for plugins

    At a minimum, the class attributes 'name', 'summary', and 'description'
    must be defined, and in most cases 'model' must also. The class attributes
    'interval' and 'backlog_delta' are optional but may be extremely useful to
    plugins.

    The 'interval' attribute may be a 'datetime.timedelta' indicating what sort
    of time windows over which this plugin calculates statistics (e.g., daily
    or weekly). When the statscache framework initializes the plugin, it will
    attach an instance of 'statscache.plugins.Schedule' that is synchronized
    with the framework-wide statistics epoch (i.e., the exact moment as of
    which statistics are being computed) to the instance attribute 'schedule'.
    This prevents the possibility that the plugin's first time window will
    extend before the availability of data.

    The attribute 'backlog_delta' defines the maximum amount of backlog that
    may potentially be useful to a plugin. For instance, if a plugin is only
    interested in messages within some rolling window (e.g., all github or
    pagure messages received in the last seven days), *and* the plugin does not
    expose historical data, then it would be pointless for it to sift through a
    month's worth of data, when only the last seven days' worth would have
    sufficed.
    """
    __meta__ = abc.ABCMeta

    name = None
    summary = None
    description = None

    interval = None # this must be either None or a datetime.timedelta instance
    backlog_delta = None # how far back to process backlog (None is unlimited)
    model = None

    def __init__(self, schedule, config, model=None):
        self.workers = cpu_count()
        self.schedule = schedule
        self.config = config
        self.launched = False
        if model:
            self.model = model

        required = ['name', 'summary', 'description']
        for attr in required:
            if not getattr(self, attr):
                raise ValueError("%r must define %r" % (self, attr))

    @property
    def ident(self):
        """
        Stringify this plugin's name to use as a (hopefully) unique identifier
        """
        ident = self.name.lower().replace(" ", "-")

        bad = ['"', "'", '(', ')', '*', '&', '?', ',']
        replacements = dict(zip(bad, [''] * len(bad)))
        for a, b in replacements.items():
            ident = ident.replace(a, b)
        schedule = getattr(self, 'schedule', None)
        if schedule:
            ident += '-{}'.format(schedule)
        return ident

    def launch(self, session):
        """ Launch asynchronous workers, restoring state from model if needed

        This method is not guaranteed to be called before message processing
        begins. Currently, it is invoked after backprocessing has completed.
        The recommended usage pattern is to launch a fixed number of worker
        threads, using the plugin thread API
        """
        self.launched = True

    @abc.abstractmethod
    def process(self, message):
        """ Process a single message, synchronously """
        pass

    @abc.abstractmethod
    def update(self, session):
        """ Update the database model, synchronously """
        pass

    def latest(self, session):
        """ Get the datetime to which the model is up-to-date """
        times = [
            # This is the _actual_ latest datetime
            getattr(session.query(self.model)\
                    .order_by(self.model.timestamp.desc())\
                    .first(), 
                    'timestamp',
                    None)
        ]
        if self.backlog_delta is not None:
            # This will limit how far back to process data, if statscache has
            # been down for longer than self.backlog_delta.
            times.append(datetime.datetime.now() - self.backlog_delta)
        return max(times) # choose the more recent datetime

    def revert(self, when, session):
        """ Revert the model change(s) made as of the given datetime """
        session.query(self.model).filter(self.model.timestamp >= when).delete()

@@ -2100,43 +2100,8 @@
 e):%0A
-        self.workers = cpu_count()%0A
     
@@ -4508,8 +4508,1400 @@
 elete()%0A
+%0A%0Aclass AsyncPlugin(BasePlugin):%0A    %22%22%22 Abstract base class for plugins utilizing asynchronous workers %22%22%22%0A%0A    __meta__ = abc.ABCMeta%0A%0A    def __init__(self, *args, **kwargs):%0A        super(AsyncPlugin, self).__init__(*args, **kwargs)%0A        self.workers = cpu_count()%0A        self.queue = Queue(backlog=self.workers) # work queue%0A%0A    def launch(self, session):%0A        def spawn(error):%0A            if error is not None:%0A                log.exception(%22error in %7B!r%7D: %7B!r%7D%22.format(self.ident, error))%0A            worker = self.queue.dequeue()%0A            worker.on_success(self.work)%0A            worker.on_failure(spawn)%0A%0A        self.fill(session)%0A%0A        for _ in xrange(self.workers):%0A            spawn(None)%0A%0A        super(AsyncPlugin, self).launch(session)%0A%0A    @abc.abstractmethod%0A    def work(self, item):%0A        %22%22%22 Perform further processing on the model item, synchronously%0A%0A        The mechanism for storage of the results of this processing for%0A        eventual commit by 'self.update()', is completely up to the individual%0A        plugin implementation.%0A%0A        Note that this method is called in its own thread, and may therefore%0A        utilize blocking I/O without disrupting any other components.%0A        %22%22%22%0A        pass%0A%0A    @abc.abstractmethod%0A    def fill(self, session):%0A        %22%22%22 Fill the work queue 'self.queue' using the initial model state %22%22%22%0A        pass%0A

Create asychronous plugin abstract base class