summaryrefslogtreecommitdiff
path: root/src/mailman/app/workflow.py
diff options
context:
space:
mode:
authorBarry Warsaw2015-04-15 10:05:35 -0400
committerBarry Warsaw2015-04-15 10:05:35 -0400
commit67fd8d6dbcf8849b3c2f8cb42a10aed465ace76c (patch)
tree97eca0ae33c97015e4d2fca31e7792e2a929fa8d /src/mailman/app/workflow.py
parent7317b94a0b746f0287ecbc5654ec544ce0112adb (diff)
parent3e7dffa750a3e7bb15ac10b711832696554ba03a (diff)
downloadmailman-67fd8d6dbcf8849b3c2f8cb42a10aed465ace76c.tar.gz
mailman-67fd8d6dbcf8849b3c2f8cb42a10aed465ace76c.tar.zst
mailman-67fd8d6dbcf8849b3c2f8cb42a10aed465ace76c.zip
Diffstat (limited to 'src/mailman/app/workflow.py')
-rw-r--r--src/mailman/app/workflow.py156
1 files changed, 156 insertions, 0 deletions
diff --git a/src/mailman/app/workflow.py b/src/mailman/app/workflow.py
new file mode 100644
index 000000000..b83d1c3aa
--- /dev/null
+++ b/src/mailman/app/workflow.py
@@ -0,0 +1,156 @@
+# Copyright (C) 2015 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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 General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Generic workflow."""
+
+__all__ = [
+ 'Workflow',
+ ]
+
+
+import sys
+import json
+import logging
+
+from collections import deque
+from mailman.interfaces.workflow import IWorkflowStateManager
+from zope.component import getUtility
+
+
+COMMASPACE = ', '
+log = logging.getLogger('mailman.error')
+
+
+
+class Workflow:
+ """Generic workflow."""
+
+ SAVE_ATTRIBUTES = ()
+ INITIAL_STATE = None
+
+ def __init__(self):
+ self.token = None
+ self._next = deque()
+ self.push(self.INITIAL_STATE)
+ self.debug = False
+ self._count = 0
+
+ @property
+ def name(self):
+ return self.__class__.__name__
+
+ def __iter__(self):
+ return self
+
+ def push(self, step):
+ self._next.append(step)
+
+ def _pop(self):
+ name = self._next.popleft()
+ step = getattr(self, '_step_{}'.format(name))
+ self._count += 1
+ if self.debug:
+ print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr)
+ return name, step
+
+ def __next__(self):
+ try:
+ name, step = self._pop()
+ return step()
+ except IndexError:
+ raise StopIteration
+ except:
+ log.exception('deque: {}'.format(COMMASPACE.join(self._next)))
+ raise
+
+ def run_thru(self, stop_after):
+ """Run the state machine through and including the given step.
+
+ :param stop_after: Name of method, sans prefix to run the
+ state machine through. In other words, the state machine runs
+ until the named method completes.
+ """
+ results = []
+ while True:
+ try:
+ name, step = self._pop()
+ except (StopIteration, IndexError):
+ # We're done.
+ break
+ results.append(step())
+ if name == stop_after:
+ break
+ return results
+
+ def run_until(self, stop_before):
+ """Trun the state machine until (not including) the given step.
+
+ :param stop_before: Name of method, sans prefix that the
+ state machine is run until the method is reached. Unlike
+ `run_thru()` the named method is not run.
+ """
+ results = []
+ while True:
+ try:
+ name, step = self._pop()
+ except (StopIteration, IndexError):
+ # We're done.
+ break
+ if name == stop_before:
+ # Stop executing, but not before we push the last state back
+ # onto the deque. Otherwise, resuming the state machine would
+ # skip this step.
+ self._next.appendleft(step)
+ break
+ results.append(step())
+ return results
+
+ def save(self):
+ assert self.token, 'Workflow token must be set'
+ state_manager = getUtility(IWorkflowStateManager)
+ data = {attr: getattr(self, attr) for attr in self.SAVE_ATTRIBUTES}
+ # Note: only the next step is saved, not the whole stack. This is not
+ # an issue in practice, since there's never more than a single step in
+ # the queue anyway. If we want to support more than a single step in
+ # the queue *and* want to support state saving/restoring, change this
+ # method and the restore() method.
+ if len(self._next) == 0:
+ step = None
+ elif len(self._next) == 1:
+ step = self._next[0]
+ else:
+ raise AssertionError(
+ "Can't save a workflow state with more than one step "
+ "in the queue")
+ state_manager.save(
+ self.__class__.__name__,
+ self.token,
+ step,
+ json.dumps(data))
+
+ def restore(self):
+ state_manager = getUtility(IWorkflowStateManager)
+ state = state_manager.restore(self.__class__.__name__, self.token)
+ if state is None:
+ # The token doesn't exist in the database.
+ raise LookupError(self.token)
+ self._next.clear()
+ if state.step:
+ self._next.append(state.step)
+ if state.data is not None:
+ for attr, value in json.loads(state.data).items():
+ setattr(self, attr, value)