spiff-arena/doc/bpmn/synthesis.rst

224 lines
9.1 KiB
ReStructuredText
Raw Normal View History

Putting it All Together
=======================
In this section we'll be discussing the overall structure of the workflow
runner we developed in `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
Loading a Workflow
-------------------
We'll need the following imports:
.. code:: python
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
from custom_script_engine import CustomScriptEngine
We need to create a parser. We could have imported :code:`BpmnParser`, which
these parsers inherit from, but we need some additional features that the base
parser does not provide.
.. code:: python
class Parser(BpmnDmnParser):
OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES
OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES)
We'll use :code:`BpmnDmnParser` as our base class, because we would like the ability
to use DMN tables in our application. The :code:`BpmnDmnParser` provides a task
parser for Business Rule Tasks, which the underlying :code:`BpmnParser` it inherits from
does not contain.
We also imported the :code:`CamundaParser` so that we can parse some Camunda
specific features we'll use (forms in User Tasks). The :code:`CamundaParser` User
Task parser will override the default parser.
In general, any task parser can be replaced with a custom parser of your
own design if you have a BPMN modeller that produces XML not handled by the
BPMN parsers in SpiffWorkflow.
.. code:: python
def parse(process, bpmn_files, dmn_files):
parser = Parser()
parser.add_bpmn_files(bpmn_files)
if dmn_files:
parser.add_dmn_files(dmn_files)
top_level = parser.get_spec(process)
subprocesses = parser.get_process_specs()
return BpmnWorkflow(top_level, subprocesses, script_engine=CustomScriptEngine)
We create an instance of our previously defined parser, add the BPMN files to it, and
optionally add any DMN files, if they were supplied.
We'll obtain the workflow specification from the parser for the top level process
using :code:`parser.get_spec()`.
We'll get the specs of all the processes that were parsed with :code:`parser.get_process_specs()`
and provide these to the workflow as well. If your entire workflow is contained in your
top-level process, you can omit this argument, but if your workflow contains call activities,
you'll need to include it.
We also provide an enhanced script engine to our workflow. More information about how and
why you might want to do this is covered in :doc:`advanced`. The :code:`script_engine`
argument is optional and the default will be used if none is supplied.
We return a :code:`BpmnWorkflow` based on the specs that uses the our custom script engine
to execute script tasks and evaluate expressions.
Running a Workflow
------------------
This is our application's :code:`run()` method.
The :code:`step` argument is a boolean that indicates whether we want the option of seeing
a more detailed representation of the state at each step, which we'll discuss in the
section following this one.
.. code:: python
def run(workflow, step):
workflow.do_engine_steps()
while not workflow.is_completed():
ready_tasks = workflow.get_ready_user_tasks()
options = { }
print()
for idx, task in enumerate(ready_tasks):
option = format_task(task, False)
options[str(idx + 1)] = task
print(f'{idx + 1}. {option}')
selected = None
while selected not in options and selected not in ['', 'D', 'd']:
selected = input('Select task to complete, enter to wait, or D to dump the workflow state: ')
if selected.lower() == 'd':
filename = input('Enter filename: ')
state = BpmnSerializer().serialize_workflow(workflow, include_spec=True)
with open(filename, 'w') as dump:
dump.write(state)
elif selected != '':
next_task = options[selected]
if isinstance(next_task.task_spec, UserTask):
complete_user_task(next_task)
next_task.complete()
elif isinstance(next_task.task_spec, ManualTask):
complete_manual_task(next_task)
next_task.complete()
else:
next_task.complete()
workflow.refresh_waiting_tasks()
workflow.do_engine_steps()
if step:
print_state(workflow)
print('\nWorkflow Data')
print(json.dumps(workflow.data, indent=2, separators=[ ', ', ': ' ]))
The first line of this function is the one that does the bulk of the work in
SpiffWorkflow. Calling :code:`workflow.do_engine_steps()` causes Spiff to repeatedly
look for and execute any engine tasks that are ready.
An **engine task** does not require user interaction. For instance, it could be
a Script task or selection of a flow from a gateway. Execution will
stop when only interactive tasks remain or the workflow is completed.
A SpiffWorkflow application will call :code:`workflow.do_engine_steps()` to start the
workflow and then enter a loop that will
- check for ready user tasks
- present the tasks to the user to complete
- complete the tasks
- refresh any waiting tasks
- complete any engine tasks that have been reached via user interactions
until the workflow completes.
When a workflow completes, the task data (just a dictionary passed from one task to the
next, and optionally modified by each task) is copied into the workflow data. We display
the end state of the workflow on completion.
The rest of the code is all about presenting the tasks to the user and dumping the
workflow state. We've covered former in the BPMN Elements section of :doc:`index`
and will cover the latter in :doc:`advanced`.
Handling task presentation is what **you** will be developing when you use SpiffWorkflow.
Examining the Workflow State
----------------------------
When this application is run and we want to present steps to the user, we'll need
to be able to examine the workflow and task states and associated data. We'll cover
the basics of this in this section.
The code below is a simple method for displaying information about a task. We use
this in two ways
- presenting a list of tasks to a user (in this case the state will always be ready, so we won't include it)
- presenting the state of each task while stepping through the workflow (in this case you most likely do want to know the state).
.. code:: python
def format_task(task, include_state=True):
if hasattr(task.task_spec, 'lane') and task.task_spec.lane is not None:
lane = f'[{task.task_spec.lane}]'
else:
lane = ''
state = f'[{task.get_state_name()}]' if include_state else ''
return f'{lane} {task.task_spec.description} ({task.task_spec.name}) {state}'
We previously went over obtaining the lane information in :doc:`organization`.
We can call :code:`task.get_state_name()` to get a human-readable representation of
a task's state.
We store the value provided in the :code:`name` attribute of the task (the text
entered in the 'Name' field in our sample models) in :code:`task.task_spec.description`.
Here is the code we use for examining the workflow state.
.. code:: python
def print_state(workflow):
task = workflow.last_task
print('\nLast Task')
print(format_task(task))
print(json.dumps(task.data, indent=2, separators=[ ', ', ': ' ]))
display_types = (UserTask, ManualTask, ScriptTask, ThrowingEvent, CatchingEvent)
all_tasks = [ task for task in workflow.get_tasks() if isinstance(task.task_spec, display_types) ]
upcoming_tasks = [ task for task in all_tasks if task.state in [Task.READY, Task.WAITING] ]
print('\nUpcoming Tasks')
for idx, task in enumerate(upcoming_tasks):
print(format_task(task))
if input('\nShow all tasks? ').lower() == 'y':
for idx, task in enumerate(all_tasks):
print(format_task(task))
We can find out what the last task was with :code:`workflow.last_task`. We'll print
its information as described above, as well as a dump of its data.
We can get a list of all tasks regardless of type or state with :code:`workflow.get_tasks()`.
The actual list of tasks will get quite long (some tasks are expanded internally by Spiff into
multiple tasks, and all gateways and events are also treated as "tasks"). So we're filtering
the tasks to only display the ones that would have salience to a user here.
We'll further filter those tasks for :code:`READY` and :code:`WAITING` tasks for a more
compact display, and only show all tasks when explicitly called for.
This is a very simple application, so our interactions with tasks are very basic. You will
definitely want to see the 'Navigation List' section of :doc:`advanced` for more sophisticated
ways of managing workflow state.