From 8b09113eb40c39ada3dc902cb4e869c8f012c97d Mon Sep 17 00:00:00 2001 From: J08nY Date: Thu, 29 Jun 2017 23:51:47 +0200 Subject: Create mailman.workflows package. Move base Workflow there. - Also introduce IWorkflow, ISubscriptionWorkflow, IUnsubscriptionWorkflow. --- src/mailman/app/subscriptions.py | 4 +- src/mailman/app/tests/test_workflow.py | 12 +-- src/mailman/app/workflow.py | 151 ------------------------------- src/mailman/config/configure.zcml | 4 +- src/mailman/interfaces/workflow.py | 67 -------------- src/mailman/interfaces/workflows.py | 128 ++++++++++++++++++++++++++ src/mailman/model/tests/test_workflow.py | 2 +- src/mailman/model/workflow.py | 71 --------------- src/mailman/model/workflows.py | 71 +++++++++++++++ src/mailman/workflows/__init__.py | 0 src/mailman/workflows/base.py | 143 +++++++++++++++++++++++++++++ 11 files changed, 353 insertions(+), 300 deletions(-) delete mode 100644 src/mailman/app/workflow.py delete mode 100644 src/mailman/interfaces/workflow.py create mode 100644 src/mailman/interfaces/workflows.py delete mode 100644 src/mailman/model/workflow.py create mode 100644 src/mailman/model/workflows.py create mode 100644 src/mailman/workflows/__init__.py create mode 100644 src/mailman/workflows/base.py diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index 0d8176ffb..dace9ccb6 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -24,7 +24,6 @@ from datetime import timedelta from email.utils import formataddr from enum import Enum from mailman.app.membership import delete_member -from mailman.app.workflow import Workflow from mailman.core.i18n import _ from mailman.database.transaction import flush from mailman.email.message import UserNotification @@ -43,9 +42,10 @@ from mailman.interfaces.subscriptions import ( from mailman.interfaces.template import ITemplateLoader from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager -from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.interfaces.workflows import IWorkflowStateManager from mailman.utilities.datetime import now from mailman.utilities.string import expand, wrap +from mailman.workflows.base import Workflow from public import public from zope.component import getUtility from zope.event import notify diff --git a/src/mailman/app/tests/test_workflow.py b/src/mailman/app/tests/test_workflow.py index e82001540..f242a73ec 100644 --- a/src/mailman/app/tests/test_workflow.py +++ b/src/mailman/app/tests/test_workflow.py @@ -20,15 +20,15 @@ import json import unittest -from mailman.app.workflow import Workflow -from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.interfaces.workflows import IWorkflowStateManager from mailman.testing.layers import ConfigLayer +from mailman.workflows.base import Workflow from zope.component import getUtility class MyWorkflow(Workflow): - INITIAL_STATE = 'first' - SAVE_ATTRIBUTES = ('ant', 'bee', 'cat') + initial_state = 'first' + save_attributes = ('ant', 'bee', 'cat') def __init__(self): super().__init__() @@ -51,7 +51,7 @@ class MyWorkflow(Workflow): class DependentWorkflow(MyWorkflow): - SAVE_ATTRIBUTES = ('ant', 'bee', 'cat', 'elf') + save_attributes = ('ant', 'bee', 'cat', 'elf') def __init__(self): super().__init__() @@ -136,7 +136,7 @@ class TestWorkflow(unittest.TestCase): def test_save_and_restore_dependant_attributes(self): # Attributes must be restored in the order they are declared in - # SAVE_ATTRIBUTES. + # save_attributes. workflow = iter(DependentWorkflow()) workflow.elf = 6 workflow.save() diff --git a/src/mailman/app/workflow.py b/src/mailman/app/workflow.py deleted file mode 100644 index 92b78c184..000000000 --- a/src/mailman/app/workflow.py +++ /dev/null @@ -1,151 +0,0 @@ -# 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 . - -"""Generic workflow.""" - -import sys -import json -import logging - -from collections import deque -from mailman.interfaces.workflow import IWorkflowStateManager -from public import public -from zope.component import getUtility - - -COMMASPACE = ', ' -log = logging.getLogger('mailman.error') - - -@public -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: # pragma: nocover - 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(name) - 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.token, step, json.dumps(data)) - - def restore(self): - 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 diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index 2fd0c8788..da4eb36b6 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -135,8 +135,8 @@ /> diff --git a/src/mailman/interfaces/workflow.py b/src/mailman/interfaces/workflow.py deleted file mode 100644 index 5b3582b58..000000000 --- a/src/mailman/interfaces/workflow.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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 . - -"""Interfaces describing the state of a workflow.""" - -from public import public -from zope.interface import Attribute, Interface - - -@public -class IWorkflowState(Interface): - """The state of a workflow.""" - - token = Attribute('A unique key identifying the workflow instance.') - - step = Attribute("This workflow's next step.") - - data = Attribute('Additional data (may be JSON-encoded).') - - -@public -class IWorkflowStateManager(Interface): - """The workflow states manager.""" - - def save(token, step, data=None): - """Save the state of a workflow. - - :param token: A unique token identifying this workflow instance. - :type token: str - :param step: The next step for this workflow. - :type step: str - :param data: Additional data (workflow-specific). - :type data: str - """ - - def restore(token): - """Get the saved state for a workflow or None if nothing was saved. - - :param token: A unique token identifying this workflow instance. - :type token: str - :return: The saved state associated with this name/token pair, or None - if the pair isn't in the database. - :rtype: ``IWorkflowState`` - """ - - def discard(token): - """Throw away the saved state for a workflow. - - :param token: A unique token identifying this workflow instance. - :type token: str - """ - - count = Attribute('The number of saved workflows in the database.') diff --git a/src/mailman/interfaces/workflows.py b/src/mailman/interfaces/workflows.py new file mode 100644 index 000000000..b110a481e --- /dev/null +++ b/src/mailman/interfaces/workflows.py @@ -0,0 +1,128 @@ +# 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 . + +"""Interfaces describing the a workflow, it's state and it's state manager.""" + +from public import public +from zope.interface import Attribute, Interface + + +@public +class IWorkflowState(Interface): + """The state of a workflow.""" + + token = Attribute('A unique key identifying the workflow instance.') + + step = Attribute("This workflow's next step.") + + data = Attribute('Additional data (may be JSON-encoded).') + + +@public +class IWorkflowStateManager(Interface): + """The workflow states manager.""" + + def save(token, step, data=None): + """Save the state of a workflow. + + :param token: A unique token identifying this workflow instance. + :type token: str + :param step: The next step for this workflow. + :type step: str + :param data: Additional data (workflow-specific). + :type data: str + """ + + def restore(token): + """Get the saved state for a workflow or None if nothing was saved. + + :param token: A unique token identifying this workflow instance. + :type token: str + :return: The saved state associated with this name/token pair, or None + if the pair isn't in the database. + :rtype: ``IWorkflowState`` + """ + + def discard(token): + """Throw away the saved state for a workflow. + + :param token: A unique token identifying this workflow instance. + :type token: str + """ + + count = Attribute('The number of saved workflows in the database.') + + +@public +class IWorkflow(Interface): + """A workflow.""" + + name = Attribute('The name of the workflow, must be unique.') + description = Attribute('A brief description of the workflow.') + initial_state = Attribute('The state in which the workflow starts.') + save_attributes = Attribute('The sequence of attributes of the workflow, ' + 'which are saved.') + + def __iter__(): + """Return an iterator over the steps.""" + + def __next__(): + """Run the next step from the queue. + + :return: The result of the step run. + """ + + def push(step): + """Push a step to this workflows queue.""" + + def run_thru(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. + """ + + def run_until(stop_before): + """Run 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. + """ + + def save(): + """Save the workflow in it's current state. + + Needs to have the `token` attribute set. + """ + + def restore(): + """Restore the workflow from the database. + + Needs to have the `token` attribute set. + """ + + +@public +class ISubscriptionWorkflow(IWorkflow): + """A workflow used for subscription.""" + + +@public +class IUnsubscriptionWorkflow(IWorkflow): + """A workflow used for unsubscription.""" diff --git a/src/mailman/model/tests/test_workflow.py b/src/mailman/model/tests/test_workflow.py index 9cc5446b7..41c00efc0 100644 --- a/src/mailman/model/tests/test_workflow.py +++ b/src/mailman/model/tests/test_workflow.py @@ -19,7 +19,7 @@ import unittest -from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.interfaces.workflows import IWorkflowStateManager from mailman.testing.layers import ConfigLayer from zope.component import getUtility diff --git a/src/mailman/model/workflow.py b/src/mailman/model/workflow.py deleted file mode 100644 index 3ca3412a6..000000000 --- a/src/mailman/model/workflow.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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 . - -"""Model for workflow states.""" - -from mailman.database.model import Model -from mailman.database.transaction import dbconnection -from mailman.database.types import SAUnicode -from mailman.interfaces.workflow import IWorkflowState, IWorkflowStateManager -from public import public -from sqlalchemy import Column -from zope.interface import implementer - - -@public -@implementer(IWorkflowState) -class WorkflowState(Model): - """Workflow states.""" - - __tablename__ = 'workflowstate' - - token = Column(SAUnicode, primary_key=True) - step = Column(SAUnicode) - data = Column(SAUnicode) - - -@public -@implementer(IWorkflowStateManager) -class WorkflowStateManager: - """See `IWorkflowStateManager`.""" - - @dbconnection - def save(self, store, token, step=None, data=None): - """See `IWorkflowStateManager`.""" - state = WorkflowState(token=token, step=step, data=data) - store.add(state) - - @dbconnection - def restore(self, store, token): - """See `IWorkflowStateManager`.""" - state = store.query(WorkflowState).get(token) - if state is not None: - store.delete(state) - return state - - @dbconnection - def discard(self, store, token): - """See `IWorkflowStateManager`.""" - state = store.query(WorkflowState).get(token) - if state is not None: - store.delete(state) - - @property - @dbconnection - def count(self, store): - """See `IWorkflowStateManager`.""" - return store.query(WorkflowState).count() diff --git a/src/mailman/model/workflows.py b/src/mailman/model/workflows.py new file mode 100644 index 000000000..587d3375e --- /dev/null +++ b/src/mailman/model/workflows.py @@ -0,0 +1,71 @@ +# 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 . + +"""Model for workflow states.""" + +from mailman.database.model import Model +from mailman.database.transaction import dbconnection +from mailman.database.types import SAUnicode +from mailman.interfaces.workflows import IWorkflowState, IWorkflowStateManager +from public import public +from sqlalchemy import Column +from zope.interface import implementer + + +@public +@implementer(IWorkflowState) +class WorkflowState(Model): + """Workflow states.""" + + __tablename__ = 'workflowstate' + + token = Column(SAUnicode, primary_key=True) + step = Column(SAUnicode) + data = Column(SAUnicode) + + +@public +@implementer(IWorkflowStateManager) +class WorkflowStateManager: + """See `IWorkflowStateManager`.""" + + @dbconnection + def save(self, store, token, step=None, data=None): + """See `IWorkflowStateManager`.""" + state = WorkflowState(token=token, step=step, data=data) + store.add(state) + + @dbconnection + def restore(self, store, token): + """See `IWorkflowStateManager`.""" + state = store.query(WorkflowState).get(token) + if state is not None: + store.delete(state) + return state + + @dbconnection + def discard(self, store, token): + """See `IWorkflowStateManager`.""" + state = store.query(WorkflowState).get(token) + if state is not None: + store.delete(state) + + @property + @dbconnection + def count(self, store): + """See `IWorkflowStateManager`.""" + return store.query(WorkflowState).count() diff --git a/src/mailman/workflows/__init__.py b/src/mailman/workflows/__init__.py new file mode 100644 index 000000000..e69de29bb 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 . + +"""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 -- cgit v1.2.3-70-g09d2