mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-01-14 19:44:25 +00:00
c904ee907b
01a25fc3f Merge pull request #333 from sartography/feature/ruff 99c7bd0c7 ruff linting fixes 56d170ba1 Cleaning up badges in the readme. 51c13be93 tweaking action, adding button 96275ad7c Adding a github action to run tests c6c40976a minor fix to please sonarcloud. 03316babb Merge pull request #332 from sartography/updates-for-2.0-release ab70a34b5 Release Notes for 2.0.0_rc1 f0bf79bd9 copy edits a7c726951 Release Notes for 2.0.0_rc1 5f0468ba4 Merge pull request #330 from sartography/updates-for-2.0-release b9ad24406 Mostly minor edits e284dd8e2 corrections and tweaks to documentation 4b2e62600 add more examples 1ea258c6a update spiffworkflow concepts 851d7cdf6 fix a few bugs I found while testing the example repo 7a0a6bdf8 update bpmn docs 07c153f2d save/restore nested subprocess tests 340e9983b Merge branch 'main' of github.com:sartography/spiffworkflow into main 618afbc59 It is rare to submit an update that touches upon both religion and the origins of the universe. I think, for the sake of supporting all view points we must offer the possibility that there can be a thing that is not a child, but rather the beginning of all childen, that there is a chicken to the first egg, a single original big bank. a68dec77e use raw strings for regexes using escape sequences w/ burnettk 4644f2810 Merge pull request #329 from sartography/task/remove-deprecated-functions ca65602c0 correct typo in filename 39ab83f1f remove one deprecated and unused feature 23d54e524 Merge pull request #328 from sartography/improvement/task-spec-attributes 544614aa9 change dmn bpmn_id method to property 12ad185a4 update bpmnworkflow.waiting_events to use classname aec77097d fix some typos & add a few missing licenses 4b87c6d0c add some changes that didn't get included in the merge commit 965a5d4e1 Merge branch 'main' into improvement/task-spec-attributes a844b34f9 alternate bomnworkflow.cancel 0a455cdd2 Merge pull request #327 from sartography/feature/mark_tasks_in_sub_workflows_as_future_if_reseting_to_a_task_before_subworkflow 2bda992aa cancel tasks in subprocesses and return cancelled tasks 309937362 take account that we reset the parent when checking all sub-process executions. d4bcf1290 handle nested subprocesses when resetting tasks 032bedea6 reset subprocess task when resetting a task inside the subprocess 3a6abe157 change reset workflow to drop tasks and re-predict e9cd65757 move exceptions for bpmn into bpmn package e654f2ff1 add bpmn_id and bpmn_name attributes to task specs 74bb9cf1a Found that tasks within a sub-workflow were left in a state of "READY" after resetting to task before the sub-workflow. 957a8faec make all task specs in bpmn processes bpmn tasks b6070005c create actual mixin classes & improve package structure 666a9e4e5 Merge pull request #326 from sartography/feature/boundary_event_reset_fix 9fe5ae4ad Whenever a task is reset who's parent is a "_ParentBoundaryEvent" class, reset to that parent boundary event instead, and execute it, so that all the boundary events are reset to the correct point as well. fbc071af5 remove 'is_engine_step' and use existing 'manual' attribute instead 0d8e53a25 remove unused attributes, minor parser improvements 6ae98b585 Merge pull request #325 from sartography/bugfix/make-data-objects-available-to-gateways cefcd3733 make data objects available to gateways 6060fe778 Merge pull request #324 from sartography/task/update-license efa24bed2 update license 56271f7f7 Merge pull request #323 from sartography/bugfix/handle-dash-in-dmn 6de4e7e01 Merge pull request #322 from sartography/improvement/remove-celery 6ee0668cb remove unnecessary dependencies in test 7ceae68c2 change literal '-' in DMN input to None 4cffc7e7a remove celery task and dependency 580d6e516 Merge pull request #321 from sartography/improvement/allow-duplicate-subprocess-names e4440d4df remove legacy signavio parser 477a23184 remove absolute imports from tests failing in CI 15a812a92 use process ids only when storing process specs abaf1b9e9 move parallel gateway tests to their own package 29fd2d0d9 remove some redundant, unused, or unnecessary tests & consolidate others fda1480bc remove unused CORRELATE attribute from tests 21a2fdbee remove signavio files 299c2613c Merge pull request #320 from sartography/parser_funcs 01afc9f6e PR feedback 646737834 Cleanup dfd3f8214 Add same methods for dmn 764e33ccd Rename file, fix tests 9646abca4 Add bpmn in memory parser functions and tests 58f6bd317 Merge pull request #319 from sartography/feature/better_task_order_for_sub_processes fd7c9308f By swapping the order of these lines, we can assure that a call activity is returned BEFORE the tasks that it contains, rather than after it. 0a7ec19d6 Merge pull request #318 from sartography/feature/optionally-skip-call-activities-when-parsing 3430a2e9f add option to skip parsing call activities 1b1da1dd2 Merge pull request #317 from sartography/bugfix/non-bpmn-tutorial e82345d68 remove some bpmn-related stuff from core serializer 6f9bc279c use name for inputs/outputs in base serializer -- not sure why this was ever changed git-subtree-dir: SpiffWorkflow git-subtree-split: 01a25fc3f829786c4b65d19fd0fda408de37c79f
493 lines
22 KiB
ReStructuredText
493 lines
22 KiB
ReStructuredText
A More In-Depth Look at Some of SpiffWorkflow's Features
|
|
========================================================
|
|
|
|
BPMN Task Specs
|
|
---------------
|
|
|
|
BPMN Tasks inherit quite a few attributes from :code:`SpiffWorkflow.specs.base.TaskSpec`, but only a few are used.
|
|
|
|
* `name`: the unique id of the TaskSpec, and it will correspond to the BPMN ID if that is present
|
|
* `description`: we use this attribute to provide a description of the BPMN type (the text that appears here can be overridden in the parser)
|
|
* `inputs`: a list of TaskSpec `names` that are parents of this TaskSpec
|
|
* `outputs`: a list of TaskSpec `names` that are children of this TaskSpec
|
|
* `manual`: :code:`True` if human input is required to complete tasks associated with this TaskSpec
|
|
|
|
BPMN Tasks have the following additional attributes.
|
|
|
|
* `bpmn_id`: the ID of the BPMN Task (this will be :code:`None` if the task is not visible on the diagram)
|
|
* `bpmn_name`: the BPMN name of the Task
|
|
* `lane`: the lane of the BPMN Task
|
|
* `documentation`: the contents of the BPMN `documentation` element for the Task
|
|
* `data_input_associations`: a list of incoming data object references
|
|
* `data_output_associtions`: a list of outgoing data object references
|
|
* `io_specification`: the BPMN IO specification of the Task
|
|
|
|
Filtering Tasks
|
|
---------------
|
|
|
|
Tasks by Lane
|
|
^^^^^^^^^^^^^
|
|
|
|
The :code:`workflow.get_ready_user_tasks` method optionally takes the argument `lane`, which can be used to
|
|
restrict the tasks returned to only tasks in that lane.
|
|
|
|
.. code:: python
|
|
|
|
ready_tasks = workflow.get_ready_user_tasks(lane='Customer')
|
|
|
|
will return only tasks in the 'Customer' lane in our example workflow.
|
|
|
|
Tasks by Spec Name
|
|
^^^^^^^^^^^^^^^^^^
|
|
|
|
To retrieve a list of tasks associated with a particular task spec, use :code:`workflow.get_tasks_from_spec_name`
|
|
|
|
.. code:: python
|
|
|
|
tasks = workflow.get_tasks_from_spec_name('customize_product')
|
|
|
|
will return a list containing the Call Actitivities for the customization of a product in our example workflow.
|
|
|
|
.. note::
|
|
|
|
The `name` paramter here refers to the task spec name, not the BPMN name (for visible tasks, this will
|
|
be the same as the `bpmn_id`)
|
|
|
|
Tasks by State
|
|
^^^^^^^^^^^^^^
|
|
|
|
We need to import the :code:`TaskState` object (unless you want to memorize which numbers correspond to which states).
|
|
|
|
.. code:: python
|
|
|
|
from SpiffWorkflow.task import TaskState
|
|
tasks = workflow.get_tasks(TaskState.COMPLETED)
|
|
|
|
will return a list of completed tasks.
|
|
|
|
See :doc:`../concepts` for more information about task states.
|
|
|
|
Tasks in a Subprocess or Call Activity
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The :code:`BpmnWorkflow` class maintains a dictionary of subprocesses (the key is the `id` of the Call Activity or
|
|
Subprocess Task). :code:`workflow.get_tasks` will start at the top level workflow and recurse through the subprocesses
|
|
to create a list of all tasks. It is also possible to start from a particular subprocess:
|
|
|
|
.. code:: python
|
|
|
|
tasks = workflow.get_tasks_from_spec_name('customize_product')
|
|
subprocess = workflow.get_subprocess(tasks[0])
|
|
subprocess_tasks = workflow.get_tasks(workflow=subprocess)
|
|
|
|
will limit the list of returned tasks to only those in the first product customization.
|
|
|
|
.. note::
|
|
|
|
Each :code:`Task` object has a reference to its workflow; so with a Task inside a subprocess, we can call
|
|
:code:`workflow.get_tasks(workflow=task.workflow)` to start from our current workflow.
|
|
|
|
Logging
|
|
-------
|
|
|
|
Spiff provides several loggers:
|
|
- the :code:`spiff` logger, which emits messages when a workflow is initialized and when tasks change state
|
|
- the :code:`spiff.metrics` logger, which emits messages containing the elapsed duration of tasks
|
|
- the :code:`spiff.data` logger, which emits a message when :code:`task.update_data` is called or workflow data is retrieved or set.
|
|
|
|
Log level :code:`INFO` will provide reasonably detailed information about state changes.
|
|
|
|
As usual, log level :code:`DEBUG` will probably provide more logs than you really want
|
|
to see, but the logs will contain the task and task internal data.
|
|
|
|
Data can be included at any level less than :code:`INFO`. In our example application,
|
|
we define a custom log level
|
|
|
|
.. code:: python
|
|
|
|
logging.addLevelName(15, 'DATA')
|
|
|
|
so that we can see the task data in the logs without fully enabling debugging.
|
|
|
|
The workflow runners take an `-l` argument that can be used to specify the logging level used when running the example workflows.
|
|
|
|
We'll write the logs to a file called `data.log` instead of the console to avoid printing very long messages during the workflow.
|
|
|
|
Our logging configuration code can be found in `runner/shared.py`. Most of the code is about logging
|
|
configuration in Python rather than anything specific to SpiffWorkflow, so we won't go over it in depth.
|
|
|
|
Parsing
|
|
-------
|
|
|
|
Each of the BPMN pacakges (:code:`bpmn`, :code:`spiff`, or :code:`camunda`) has a parser that is preconfigured with
|
|
the specs in that package (if a particular TaskSpec is not implemented in the package, :code:`bpmn` TaskSpec is used).
|
|
|
|
See the example in :doc:`synthesis` for the basics of creating a parser. The parser can optionally be initialized with
|
|
|
|
- a set of namespaces (useful if you have custom extensions)
|
|
- a BPMN Validator (the one in the :code:`bpmn` package validates against the BPMN 2.0 spec)
|
|
- a mapping of XML tag to Task Spec Descriptions. The default set of descriptions can be found in
|
|
:code:`SpiffWorkflow.bpmn.parser.spec_descriptions`. These values will be added to the Task Spec in the `description` attribute
|
|
and are intended as a user-friendly description of what the task is.
|
|
|
|
The :code:`BpmnValidator` can be used and extended independently of the parser as well; call :code:`validate` with
|
|
an :code:`lxml` parsed tree.
|
|
|
|
Loading BPMN Files
|
|
^^^^^^^^^^^^^^^^^^
|
|
|
|
In addition to :code:`load_bpmn_file`, there are similar functions :code:`load_bpmn_str` which can load the XML from a string, and
|
|
:code:`load_bpmn_io`, which can load XML from any object implementing the IO interface, and :code:`add_bpmn_xml`, which can load
|
|
BPMN specs from an :code:`lxml` parsed tree.
|
|
|
|
Dependencies
|
|
^^^^^^^^^^^^
|
|
|
|
The following methods are available for discovering the names of processes and DMN files that may be defined externally:
|
|
|
|
- :code:`get_subprocess_specs`: Returns a mapping of name -> :code:`BpmnWorkflowSpec` for any Call Activities referenced by the
|
|
provided spec (searches recursively)
|
|
- :code:`find_all_spec`: Returns a mapping of name -> :code:`BpmnWorkflowSpec` for all processes used in all files that have been
|
|
provided to the parser at that point.
|
|
- :code:`get_process_dependences`: Returns a list of process IDs referenced by the provided process ID
|
|
- :code:`get_dmn_dependencies`: Returns a list of DMN IDs referenced by the provided process ID
|
|
|
|
|
|
Serialization
|
|
-------------
|
|
|
|
The :code:`BpmnWorkflowSerializer` has two components
|
|
|
|
* the `workflow_spec_converter` (which handles serialization of objects that SpiffWorkflow knows about)
|
|
* the `data_converter` (which handles serialization of custom objects)
|
|
|
|
Unless you have overriden any of TaskSpecs with custom specs, you should be able to use the serializer
|
|
configuration from the package you are importing the parser from (:code:`bpmn`, :code:`spiff`, or :code:`camunda`).
|
|
See :doc:`synthesis` for an example.
|
|
|
|
Serializing Custom Objects
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
In `Custom Script Engines`_ , we add some custom methods and objects to our scripting environment. We create a simple
|
|
class (a :code:`namedtuple`) that holds the product information for each product.
|
|
|
|
We'd like to be able to save and restore our custom object.
|
|
|
|
.. code:: python
|
|
|
|
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
|
|
|
|
def product_info_to_dict(obj):
|
|
return {
|
|
'color': obj.color,
|
|
'size': obj.size,
|
|
'style': obj.style,
|
|
'price': obj.price,
|
|
}
|
|
|
|
def product_info_from_dict(dct):
|
|
return ProductInfo(**dct)
|
|
|
|
registry = DictionaryConverter()
|
|
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
|
|
|
|
Here we define two functions, one for turning our object into a dictionary of serializable objects, and one for recreating
|
|
the object from the dictionary representation we created.
|
|
|
|
We initialize a :code:`DictionaryConverter` and `register` the class and methods.
|
|
|
|
Registering an object sets up relationships between the class and the serialization and deserialization methods. We go
|
|
over how this works in a little more detail in `Custom Serialization in More Depth`_.
|
|
|
|
It is also possible to bypass using a :code:`DictionaryConverter` at all for the data serialization process (but not for
|
|
the spec serialization process). The only requirement for the the `data_converter` is that it implement the methods
|
|
|
|
- `convert`, which takes an object and returns something JSON-serializable
|
|
- `restore`, which takes a serialized version and returns an object
|
|
|
|
Serialization Versions
|
|
^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
As we make changes to Spiff, we may change the serialization format. For example, in 1.2.1, we changed
|
|
how subprocesses were handled interally in BPMN workflows and updated how they are serialized and we upraded the
|
|
serializer version to 1.1.
|
|
|
|
As we release SpiffWorkflow 2.0, there are several more substantial changes, and we'll upgrade the serializer version to 1.2.
|
|
|
|
Since workflows can contain arbitrary data, and even SpiffWorkflow's internal classes are designed to be customized in ways
|
|
that might require special serialization and deserialization, it is possible to override the default version number, to
|
|
provide users with a way of tracking their own changes. This can be accomplished by setting the `VERSION` attribute on
|
|
the :code:`BpmnWorkflowSerializer` class.
|
|
|
|
If you have not provided a custom version number, SpiffWorkflow wil attempt to migrate your workflows from one version
|
|
to the next if they were serialized in an earlier format.
|
|
|
|
If you've overridden the serializer version, you may need to incorporate our serialization changes with
|
|
your own. You can find our conversions in
|
|
`SpiffWorkflow/bpmn/serilaizer/migrations <https://github.com/sartography/SpiffWorkflow/tree/main/SpiffWorkflow/bpmn/serializer/migration>`_
|
|
|
|
These are broken up into functions that handle each individual change, which will hopefully make it easier to incoporate them
|
|
into your upgrade process, and also provides some documentation on what has changed.
|
|
|
|
Custom Serialization in More Depth
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Both of the serializer components mentioned in `Serialization`_ are based on the :code:`DictionaryConverter`. Let's import
|
|
it and create one and register a type:
|
|
|
|
.. code:: python
|
|
|
|
from datetime import datetime
|
|
|
|
from SpiffWorkflow.bpmn.serializer.helpers.dictionary import DictionaryConverter
|
|
registry = DictionaryConverter()
|
|
registry.register(
|
|
datetime.
|
|
lambda dt: {'value': dt.isoformat() },
|
|
lambda dct: datetime.fromisoformat(dct['value'])
|
|
)
|
|
|
|
The arguemnts to :code:`register` are:
|
|
|
|
* `cls`: the class to be converted
|
|
* `to_dict`: a function that returns a dictionary containing JSON-serializable objects
|
|
* `from_dict`: a function that take the output of `to_dict` and restores the original object
|
|
|
|
When the :code:`register` method is called, a `typename` is created and maps are set up between `cls` and `to_dict`
|
|
function, `cls` and `typename`, and `typename` and `from_dict`.
|
|
|
|
When :code:`registry.convert` is called on an object, the `cls` is use to retrieve the `to_dict` function and the
|
|
`typename`. The `to_dict` funciton is called on the object and the `typename` is added to the resulting dictionary.
|
|
|
|
When :code:`registry.restore` is called with a dictionary, it is checked for a `typename` key, and if one exists, it
|
|
is used to retrieve the `from_dict` function and the dictionary is passed to it.
|
|
|
|
If an object is not recognized, it will be passed on as-is.
|
|
|
|
Custom Script Engines
|
|
---------------------
|
|
|
|
You may need to modify the default script engine, whether because you need to make additional
|
|
functionality available to it, or because you might want to restrict its capabilities for
|
|
security reasons.
|
|
|
|
.. warning::
|
|
|
|
By default, the scripting environment passes input directly to :code:`eval` and :code:`exec`! In most
|
|
cases, you'll want to replace the default scripting environment with one of your own.
|
|
|
|
Files referenced in this section:
|
|
|
|
* `script_engine.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/script_engine.py>`_
|
|
* `product_info.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/product_info.py>`_
|
|
* `subprocess.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/subprocess.py>`_
|
|
* `spiff-bpmn-runner.py <https://github.com/sartography/spiff-example-cli/blob/main/spiff-bpmn-runner.py>`_
|
|
|
|
The following example replaces the default global enviroment with the one provided by
|
|
`RestrictedPython <https://restrictedpython.readthedocs.io/en/latest/>`_
|
|
|
|
.. code:: python
|
|
|
|
from RestrictedPython import safe_globals
|
|
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
|
|
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import TaskDataEnvironment
|
|
|
|
restricted_env = TaskDataEnvironment(safe_globals)
|
|
restricted_script_engine = PythonScriptEngine(environment=restricted_env)
|
|
|
|
Another reason you might want to customize the scripting environment is to provide access to custom
|
|
classes or functions.
|
|
|
|
In our example models so far, we've been using DMN tables to obtain product information. DMN
|
|
tables have a **lot** of uses so we wanted to feature them prominently, but in a simple way.
|
|
|
|
If a customer was selecting a product, we would surely have information about how the product
|
|
could be customized in a database somewhere. We would not hard code product information in
|
|
our diagram (although it is much easier to modify the BPMN diagram than to change the code
|
|
itself!). Our shipping costs would not be static, but would depend on the size of the order and
|
|
where it was being shipped -- maybe we'd query an API provided by our shipper.
|
|
|
|
SpiffWorkflow is obviously **not** going to know how to query **your** database or make API calls to
|
|
**your** vendors. However, one way of making this functionality available inside your diagram is to
|
|
implement the calls in functions and add those functions to the scripting environment, where they
|
|
can be called by Script Tasks.
|
|
|
|
We are not going to actually include a database or API and write code for connecting to and querying
|
|
it, but since we only have 7 products we can model our database with a simple dictionary lookup
|
|
and just return the same static info for shipping for the purposes of the tutorial.
|
|
|
|
We'll define some resources in `product_info.py`
|
|
|
|
.. code:: python
|
|
|
|
from collections import namedtuple
|
|
|
|
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
|
|
|
|
INVENTORY = {
|
|
'product_a': ProductInfo(False, False, False, 15.00),
|
|
'product_b': ProductInfo(False, False, False, 15.00),
|
|
'product_c': ProductInfo(True, False, False, 25.00),
|
|
'product_d': ProductInfo(True, True, False, 20.00),
|
|
'product_e': ProductInfo(True, True, True, 25.00),
|
|
'product_f': ProductInfo(True, True, True, 30.00),
|
|
'product_g': ProductInfo(False, False, True, 25.00),
|
|
}
|
|
|
|
def lookup_product_info(product_name):
|
|
return INVENTORY[product_name]
|
|
|
|
def lookup_shipping_cost(shipping_method):
|
|
return 25.00 if shipping_method == 'Overnight' else 5.00
|
|
|
|
We'll add these functions to our scripting environment in `script_engine.py`
|
|
|
|
.. code:: python
|
|
|
|
env_globals = {
|
|
'lookup_product_info': lookup_product_info,
|
|
'lookup_shipping_cost': lookup_shipping_cost,
|
|
'datetime': datetime,
|
|
}
|
|
custom_env = TaskDataEnvironment(env_globals)
|
|
custom_script_engine = PythonScriptEngine(environment=custom_env)
|
|
|
|
.. note::
|
|
|
|
We're also adding :code:`datetime`, because we added the timestamp to the payload of our message when we
|
|
set up the Message Event (see :doc:`events`)
|
|
|
|
When we initialize the runner in `spiff-bpmn-runner.py`, we'll import and use `cusrom_script_engine` as our
|
|
script engine.
|
|
|
|
We can use the custom functions in script tasks like any normal function. We've replaced the Business Rule
|
|
Task that determines product price with a script that simply checks the `price` field on our product.
|
|
|
|
.. figure:: figures/script_engine/top_level.png
|
|
:scale: 30%
|
|
:align: center
|
|
|
|
Top Level Workflow with Custom Script Engine
|
|
|
|
And we can simplify the gateways in our 'Call Activity' flows as well now too:
|
|
|
|
.. figure:: figures/script_engine/call_activity.png
|
|
:scale: 30%
|
|
:align: center
|
|
|
|
Call Activity with Custom Script Engine
|
|
|
|
To run this workflow (you'll have to manually change which script engine you import):
|
|
|
|
.. code-block:: console
|
|
|
|
./spiff-bpmn-runner.py -p order_product -b bpmn/tutorial/top_level_script.bpmn bpmn/tutorial/call_activity_script.bpmn
|
|
|
|
Another reason to customize the scripting enviroment is to allow it to run completely separately from
|
|
SpiffWorkflow. You might wish to do this for performance or security reasons.
|
|
|
|
In our example repo, we've created a simple command line script in `runner/subprocess.py` that takes serialized global
|
|
and local environments and a script or expression to execute or evaluate. In `runner/script_engine.py`, we create
|
|
a scripting environment that runs the current :code:`execute` or :code:`evaluate` request in a subprocess with this
|
|
script. We've imported our custom methods into `subprocess.py` so they are automatically available when it is used.
|
|
|
|
This example is needlessly complex for the work we're doing in this case, but the point of the example is to demonstrate
|
|
that this could be a Docker container with a complex environment, an HTTP API running somewhere else entirely.
|
|
|
|
.. note::
|
|
|
|
Note that our execute method returns :code:`True`. We could check the status of our process here and return
|
|
:code:`False` to force our task into an `ERROR` state if the task failed to execute.
|
|
|
|
We could also return :code:`None`
|
|
if the task is not finished; this will cause the task to go into the `STARTED` state. You would have to manually
|
|
complete a task that has been `STARTED`. The purpose of the state is to tell SpiffWorkflow your application will
|
|
handle monitoring and updating this task and other branches that do not depend on this task may proceed. It is
|
|
intended to be used with potentially long-running tasks.
|
|
|
|
See :doc:`../concepts` for more information about Task States and Workflow execution.
|
|
|
|
Service Tasks
|
|
-------------
|
|
|
|
Service Tasks are also executed by the workflow's script engine, but through a different method, with the help of some
|
|
custom extensions in the :code:`spiff` package:
|
|
|
|
- `operation_name`, the name assigned to the service being called
|
|
- `operation_params`, the parameters the operation requires
|
|
|
|
|
|
This is our script engine and scripting environment:
|
|
|
|
.. code:: python
|
|
|
|
service_task_env = TaskDataEnvironment({
|
|
'product_info_from_dict': product_info_from_dict,
|
|
'datetime': datetime,
|
|
})
|
|
|
|
class ServiceTaskEngine(PythonScriptEngine):
|
|
|
|
def __init__(self):
|
|
super().__init__(environment=service_task_env)
|
|
|
|
def call_service(self, operation_name, operation_params, task_data):
|
|
if operation_name == 'lookup_product_info':
|
|
product_info = lookup_product_info(operation_params['product_name']['value'])
|
|
result = product_info_to_dict(product_info)
|
|
elif operation_name == 'lookup_shipping_cost':
|
|
result = lookup_shipping_cost(operation_params['shipping_method']['value'])
|
|
else:
|
|
raise Exception("Unknown Service!")
|
|
return json.dumps(result)
|
|
|
|
service_task_engine = ServiceTaskEngine()
|
|
|
|
Instead of adding our custom functions to the enviroment, we'll override :code:`call_service` and call them directly
|
|
according to the `operation_name` that was given. The :code:`spiff` Service Task also evaluates the parameters
|
|
against the task data for us, so we can pass those in directly. The Service Task will also store our result in
|
|
a user-specified variable.
|
|
|
|
We need to send the result back as json, so we'll reuse the functions we wrote for the serializer.
|
|
|
|
The Service Task will assign the dictionary as the operation result, so we'll add a `postScript` to the Service Task
|
|
that retrieves the product information that creates a :code:`ProductInfo` instance from the dictionary, so we need to
|
|
import that too.
|
|
|
|
The XML for the Service Task looks like this:
|
|
|
|
.. code:: xml
|
|
|
|
<bpmn:serviceTask id="Activity_1ln3xkw" name="Lookup Product Info">
|
|
<bpmn:extensionElements>
|
|
<spiffworkflow:serviceTaskOperator id="lookup_product_info" resultVariable="product_info">
|
|
<spiffworkflow:parameters>
|
|
<spiffworkflow:parameter id="product_name" type="str" value="product_name"/>
|
|
</spiffworkflow:parameters>
|
|
</spiffworkflow:serviceTaskOperator>
|
|
<spiffworkflow:postScript>product_info = product_info_from_dict(product_info)</spiffworkflow:postScript>
|
|
</bpmn:extensionElements>
|
|
<bpmn:incoming>Flow_104dmrv</bpmn:incoming>
|
|
<bpmn:outgoing>Flow_06k811b</bpmn:outgoing>
|
|
</bpmn:serviceTask>
|
|
|
|
Getting this information into the XML is a little bit beyond the scope of this tutorial, as it involves more than
|
|
just SpiffWorkflow. I hand edited it for this case, but you can hardly ask your BPMN authors to do that!
|
|
|
|
Our `modeler <https://github.com/sartography/bpmn-js-spiffworkflow>`_ has a means of providing a list of services and
|
|
their parameters that can be displayed to a BPMN author in the Service Task configurtion panel. There is an example of
|
|
hard-coding a list of services in
|
|
`app.js <https://github.com/sartography/bpmn-js-spiffworkflow/blob/0a9db509a0e85aa7adecc8301d8fbca9db75ac7c/app/app.js#L47>`_
|
|
and as suggested, it would be reasonably straightforward to replace this with a API call. `SpiffArena <https://www.spiffworkflow.org/posts/articles/get_started/>`_
|
|
has robust mechanisms for handling this that might serve as a model for you.
|
|
|
|
How this all works is obviously heavily dependent on your application, so we won't go into further detail here, except
|
|
to give you a bare bones starting point for implementing something yourself that meets your own needs.
|
|
|
|
To run this workflow (you'll have to manually change which script engine you import):
|
|
|
|
.. code-block:: console
|
|
|
|
./spiff-bpmn-runner.py -p order_product -b bpmn/tutorial/top_level_service_task.bpmn bpmn/tutorial/call_activity_service_task.bpmn
|
|
|