summaryrefslogtreecommitdiff
path: root/src/mailman/workflows/base.py
diff options
context:
space:
mode:
authorJ08nY2017-06-29 23:51:47 +0200
committerJ08nY2017-08-30 13:18:10 +0200
commitf2cf2d0c96c0cf47e6dfa80137bb705ec4e8321b (patch)
tree05afab72170dca745dad0d9976b28ce5878956d0 /src/mailman/workflows/base.py
parentc1060c9dfec4776ab6d714bea6e678a7d708396e (diff)
downloadmailman-f2cf2d0c96c0cf47e6dfa80137bb705ec4e8321b.tar.gz
mailman-f2cf2d0c96c0cf47e6dfa80137bb705ec4e8321b.tar.zst
mailman-f2cf2d0c96c0cf47e6dfa80137bb705ec4e8321b.zip
Diffstat (limited to 'src/mailman/workflows/base.py')
-rw-r--r--src/mailman/workflows/base.py143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/mailman/workflows/base.py b/src/mailman/workflows/base.py
new file mode 100644
index 000000000..5988dfa43
--- /dev/null
+++ b/src/mailman/workflows/base.py
@@ -0,0 +1,143 @@
+# Copyright (C) 2015-2017 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."""
+
+import sys
+import json
+import logging
+
+from collections import deque
+
+from mailman.interfaces.workflows import IWorkflowStateManager
+from public import public
+from zope.component import getUtility
+
+
+COMMASPACE = ', '
+log = logging.getLogger('mailman.error')
+
+
+@public
+class Workflow:
+ """Generic workflow."""
+
+ initial_state = None
+ save_attributes = ()
+
+ def __init__(self):
+ self.token = None
+ self._next = deque()
+ self.push(self.initial_state)
+ self.debug = False
+ self._count = 0
+
+ def __iter__(self):
+ """See `IWorkflow`."""
+ return self
+
+ def __next__(self):
+ """See `IWorkflow`."""
+ try:
+ name, step = self._pop()
+ return step()
+ except IndexError:
+ raise StopIteration
+ except:
+ log.exception('deque: {}'.format(COMMASPACE.join(self._next)))
+ raise
+
+ def push(self, step):
+ """See `IWorkflow`."""
+ self._next.append(step)
+
+ def _pop(self):
+ name = self._next.popleft()
+ step = getattr(self, '_step_{}'.format(name))
+ self._count += 1
+ if self.debug: # pragma: nocover
+ print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr)
+ return name, step
+
+ def run_thru(self, stop_after):
+ """See `IWorkflow`."""
+ 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):
+ """See `IWorkflow`."""
+ 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(name)
+ break
+ results.append(step())
+ return results
+
+ def save(self):
+ """See `IWorkflow`."""
+ 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.token, step, json.dumps(data))
+
+ def restore(self):
+ """See `IWorkflow`."""
+ state_manager = getUtility(IWorkflowStateManager)
+ state = state_manager.restore(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)
+ data = json.loads(state.data)
+ for attr in self.save_attributes:
+ try:
+ setattr(self, attr, data[attr])
+ except KeyError:
+ pass