Merge commit 'e2022f401a5968e76286645e9f4eb6ee4efb2196'

This commit is contained in:
burnettk 2023-05-29 17:31:34 -04:00
commit 73a4012a87
No known key found for this signature in database
426 changed files with 7578 additions and 151389 deletions

View File

@ -0,0 +1,32 @@
name: Unit Tests in Python 3.8, 3.9, 3.10, 3.11
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
# - name: Lint with ruff
# run: |
# stop the build if there are Python syntax errors or undefined names
# ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 .
# default set of ruff rules with GitHub Annotations
# ruff --format=github --target-version=py37 .
- name: Test with pytest
run: |
python -m unittest discover -v -s ./tests/SpiffWorkflow/ -p *Test.py

View File

@ -1,7 +0,0 @@
sonar.organization=sartography
sonar.projectKey=sartography_SpiffWorkflow
sonar.host.url=https://sonarcloud.io
sonar.exclusions=*.bpmn,*.dmn,doc/**
sonar.sources=SpiffWorkflow
sonar.test.inclusions=tests
sonar.python.coverage.reportPaths=tests/SpiffWorkflow/coverage.xml

View File

@ -57,4 +57,4 @@ New versions of SpiffWorkflow are automatically published to PyPi whenever
a maintainer of our GitHub repository creates a new release on GitHub. This a maintainer of our GitHub repository creates a new release on GitHub. This
is managed through GitHub's actions. The configuration of which can be is managed through GitHub's actions. The configuration of which can be
found in .github/workflows/.... found in .github/workflows/....
Just create a release in GitHub that mathches the release number in doc/conf.py Just create a release in GitHub that matches the release number in doc/conf.py

View File

@ -19,9 +19,7 @@ strategy for building Low-Code applications.
## Build status ## Build status
[![Build Status](https://travis-ci.com/sartography/SpiffWorkflow.svg?branch=master)](https://travis-ci.org/sartography/SpiffWorkflow) [![Build Status](https://travis-ci.com/sartography/SpiffWorkflow.svg?branch=master)](https://travis-ci.org/sartography/SpiffWorkflow)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=sartography_SpiffWorkflow&metric=alert_status)](https://sonarcloud.io/dashboard?id=sartography_SpiffWorkflow) [![SpiffWorkflow](https://github.com/sartography/SpiffWorkflow/actions/workflows/tests.yaml/badge.svg)](https://github.com/sartography/SpiffWorkflow/actions/workflows/tests.yaml)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=sartography_SpiffWorkflow&metric=coverage)](https://sonarcloud.io/dashboard?id=sartography_SpiffWorkflow)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=sartography_SpiffWorkflow&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=sartography_SpiffWorkflow)
[![Documentation Status](https://readthedocs.org/projects/spiffworkflow/badge/?version=latest)](http://spiffworkflow.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/spiffworkflow/badge/?version=latest)](http://spiffworkflow.readthedocs.io/en/latest/?badge=latest)
[![Issues](https://img.shields.io/github/issues/sartography/spiffworkflow)](https://github.com/sartography/SpiffWorkflow/issues) [![Issues](https://img.shields.io/github/issues/sartography/spiffworkflow)](https://github.com/sartography/SpiffWorkflow/issues)
[![Pull Requests](https://img.shields.io/github/issues-pr/sartography/spiffworkflow)](https://github.com/sartography/SpiffWorkflow/pulls) [![Pull Requests](https://img.shields.io/github/issues-pr/sartography/spiffworkflow)](https://github.com/sartography/SpiffWorkflow/pulls)

View File

@ -0,0 +1,138 @@
## What's Changed
We've done a lot of work over the last 8 months to the SpiffWorkflow library as we've developed [SpiffArena](https://www.spiffworkflow.org/), a general purpose workflow management system built on top of this library.
This has resulted in just a handful of new features.
Our main focus was on making SpiffWorkflow more predictable, easier to use, and internally consistent.
## Breaking Changes from 1.x:
* We heavily refactored the way we handle multi-instance tasks internally. This will break any serialized workflows that contain multi-instance tasks.
* Internal structure of our code, the names classes, and common methods have changed. Please see our [ReadTheDocs] (https://readthedocs.org/projects/spiffworkflow/) documenation for version 2.0.0.
## Features and Improvements
### Task States, Transitions, Hooks, and Execution
Previous to 2.0, SpiffWorklow was a little weird about its states, performing the actual execution in the on_complete() hook.
This was VERY confusing.
Tasks now have a _run() command separate from state change hooks.
The return value of the _run() command can be true (worked), false (failure), or None (not yet done).
This opens the door for better overall state management at the moment it is most critical (when the task is actually executing).
We also added new task state called "STARTED" that describes when a task was started, but hasn't finished yet, an oddly missing state in previous versions.
* Improvement/execution and serialization cleanup by @essweine in https://github.com/sartography/SpiffWorkflow/pull/289
* Bugfix/execute tasks on ready by @essweine in https://github.com/sartography/SpiffWorkflow/pull/303
* Feature/standardize task execution by @essweine in https://github.com/sartography/SpiffWorkflow/pull/307
* do not execute boundary events in catch by @essweine in https://github.com/sartography/SpiffWorkflow/pull/312
* Feature/new task states by @essweine in https://github.com/sartography/SpiffWorkflow/pull/315
### Improved Events
We refactored the way we handle events, making them more powerful and adaptable.
Timer events are now parsed according to the [ISO 8601 standard](https://en.wikipedia.org/wiki/ISO_8601).
* Feature/multiple event definition by @essweine in https://github.com/sartography/SpiffWorkflow/pull/268
* hacks to handle timer events like regular events by @essweine in https://github.com/sartography/SpiffWorkflow/pull/273
* Feature/improved timer events by @essweine in https://github.com/sartography/SpiffWorkflow/pull/284
* reset boundary events in loops by @essweine in https://github.com/sartography/SpiffWorkflow/pull/294
* Bugfix/execute event gateways on ready by @essweine in https://github.com/sartography/SpiffWorkflow/pull/308
### Improved Multi-Instance Tasks
We refactored how Multi-instance tasks are handled internally, vastly simplifying their representation during execution and serialization.
No more 'phantom gateways.'
* Feature/multiinstance refactor by @essweine in https://github.com/sartography/SpiffWorkflow/pull/292
### Improved SubProcesses
SpiffWorkflow did not previously distinguish between a Call Activity and a SubProcess, but they handle Data Objects very differently.
A SubProcess is now able to access its parent data objects, a Call Activity can not.
We also wanted the ability to execute Call Activities independently of the parent process.
* Bugfix/subprocess access to data objects by @essweine in https://github.com/sartography/SpiffWorkflow/pull/296
* start workflow while subprocess is waiting by @essweine in https://github.com/sartography/SpiffWorkflow/pull/302
* use same data objects & references in subprocesses after deserialization by @essweine in https://github.com/sartography/SpiffWorkflow/pull/314
### Improved Data Objects / Data Stores
This work will continue in subsequent releases, but we have added support for Data Stores, and it is possible to provide your own implementations.
* Data stores by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/298
* make data objects available to gateways by @essweine in https://github.com/sartography/SpiffWorkflow/pull/325
### Improved Inclusive Gateways
We added support for Inclusive Gateways.
* Feature/inclusive gateway support by @essweine in https://github.com/sartography/SpiffWorkflow/pull/286
### Pre and Post Script Fixes
We previously supported adding a pre-script or post-script to any task but there were a few lingering bugs that needed fixing.
* parse spiff script extensions in service tasks by @essweine in https://github.com/sartography/SpiffWorkflow/pull/257
* pass script to workflow task exec exception by @essweine in https://github.com/sartography/SpiffWorkflow/pull/258
* update execution order for postscripts by @essweine in https://github.com/sartography/SpiffWorkflow/pull/259
### DMN Improvements
We now support a new hit policy of "COLLECT" which allows you to match on an array of items. DMN support is still limited, but
we are making headway. We would love to know if people are using these features.
* Support for the "COLLECT" hit policy. by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/267
* Bugfix/handle dash in DMN by @essweine in https://github.com/sartography/SpiffWorkflow/pull/323
### BPMN Validation
We improved validation of BPMN and DMN Files to catch errors earlier.
* Feature/xml validation by @essweine and @danfunk in https://github.com/sartography/SpiffWorkflow/pull/256
### New Serializer
There are some breaking changes in the new serializer, but it is much faster and more stable. We do attempt to upgrade
your serialized workflows to the new format, but you will definitely encounter issues if you were using multi-instance tasks.
* update serializer version by @essweine in https://github.com/sartography/SpiffWorkflow/pull/277
* Feature/remove old serializer by @essweine in https://github.com/sartography/SpiffWorkflow/pull/278
### Lightning Fast, Stable Tests
* Fix ResourceWarning: unclosed file BpmnParser.py:60 by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/270
* Option to run tests in parallel by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/271
### Better Errors
* Feature/better errors by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/283
* Workflow Data Exceptions were broken in the previous error refactor. … by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/287
* added an exception for task not found w/ @burnettk by @jasquat in https://github.com/sartography/SpiffWorkflow/pull/310
* give us a better error if for some reason a task does not exist by @burnettk in https://github.com/sartography/SpiffWorkflow/pull/311
### Flexible Data Management
* Allow for other PythonScriptEngine environments besides task data by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/288
### Various Enhancements
Make it easier to reference SpiffWorkflow library classes from your own code.
* Feature/add init to schema by @jasquat in https://github.com/sartography/SpiffWorkflow/pull/260
* cleaning up code smell by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/261
* Feature/cleanup task completion by @essweine in https://github.com/sartography/SpiffWorkflow/pull/263
* disambiguate DMN expressions by @essweine in https://github.com/sartography/SpiffWorkflow/pull/264
* Add in memory BPMN/DMN parser functions by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/320
### Better Introspection
Added the ability to ask SpiffWorkflow some useful questions about a specification such as, "What call activities does this depend on?",
"What messages does this process send and receive", and "What lanes exist on this workflow specification?"
* Parser Information about messages, correlation keys, and the presence of lanes by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/262
* Called elements by @jbirddog in https://github.com/sartography/SpiffWorkflow/pull/316
### Code Cleanup
* Improvement/task spec attributes by @essweine in https://github.com/sartography/SpiffWorkflow/pull/328
* update license by @essweine in https://github.com/sartography/SpiffWorkflow/pull/324
* Feature/remove unused BPMN attributes and methods by @essweine in https://github.com/sartography/SpiffWorkflow/pull/280
* Improvement/remove camunda from base and misc cleanup by @essweine in https://github.com/sartography/SpiffWorkflow/pull/295
* remove minidom by @essweine in https://github.com/sartography/SpiffWorkflow/pull/300
* Feature/remove loop reset by @essweine in https://github.com/sartography/SpiffWorkflow/pull/305
* Feature/create core test package by @essweine in https://github.com/sartography/SpiffWorkflow/pull/306
* remove celery task and dependency by @essweine in https://github.com/sartography/SpiffWorkflow/pull/322
* remove one deprecated and unused feature by @essweine in https://github.com/sartography/SpiffWorkflow/pull/329
* change the order of tasks when calling get_tasks() by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/319
### Improved Documentation
* Fixes grammar, typos, and spellings by @rachfop in https://github.com/sartography/SpiffWorkflow/pull/291
* Updates for 2.0 release by @essweine in https://github.com/sartography/SpiffWorkflow/pull/330
* Bugfix/non BPMN tutorial by @essweine in https://github.com/sartography/SpiffWorkflow/pull/317
### Bug Fixes
* correct xpath for extensions by @essweine in https://github.com/sartography/SpiffWorkflow/pull/265
* prevent output associations from being removed twice by @essweine in https://github.com/sartography/SpiffWorkflow/pull/275
* fix for workflowspec dump by @subhakarks in https://github.com/sartography/SpiffWorkflow/pull/282
* add checks for len == 0 when copying based on io spec by @essweine in https://github.com/sartography/SpiffWorkflow/pull/297
* Improvement/allow duplicate subprocess names by @essweine in https://github.com/sartography/SpiffWorkflow/pull/321
* Resets to tasks with Boundary Events by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/326
* Sub-workflow tasks should be marked as "Future" when resetting to a task before the sub-process. by @danfunk in https://github.com/sartography/SpiffWorkflow/pull/327
## New Contributors
* @subhakarks made their first contribution in https://github.com/sartography/SpiffWorkflow/pull/282
* @rachfop made their first contribution in https://github.com/sartography/SpiffWorkflow/pull/291
**Full Changelog**: https://github.com/sartography/SpiffWorkflow/compare/v1.2.1...v2.0.0

View File

@ -0,0 +1,16 @@
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,20 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2020 Kelly McDonald, 2023 Sartography
import re
import datetime
import operator
from datetime import timedelta
from decimal import Decimal
from .PythonScriptEngine import PythonScriptEngine
# Copyright (C) 2020 Kelly McDonald
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -24,6 +17,13 @@ from .PythonScriptEngine import PythonScriptEngine
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import re
import datetime
import operator
from datetime import timedelta
from decimal import Decimal
from .PythonScriptEngine import PythonScriptEngine
def feelConvertTime(datestr,parsestr): def feelConvertTime(datestr,parsestr):
return datetime.datetime.strptime(datestr,parsestr) return datetime.datetime.strptime(datestr,parsestr)
@ -79,8 +79,8 @@ class FeelNot():
def feelConcatenate(*lst): def feelConcatenate(*lst):
ilist = [] ilist = []
for l in lst: for list_item in lst:
ilist = ilist + l ilist = ilist + list_item
return ilist return ilist
def feelAppend(lst,item): def feelAppend(lst,item):
@ -144,7 +144,7 @@ def feelFilter(var,a,b,op,column=None):
newvar.append({'key':key,'value':var[key]}) newvar.append({'key':key,'value':var[key]})
var = newvar var = newvar
if column!=None: if column is not None:
return [x.get(column) for x in var if opmap[op](x.get(a), b)] return [x.get(column) for x in var if opmap[op](x.get(a), b)]
else: else:
return [x for x in var if opmap[op](x.get(a), b)] return [x for x in var if opmap[op](x.get(a), b)]
@ -306,7 +306,7 @@ class FeelLikeScriptEngine(PythonScriptEngine):
if external_methods is None: if external_methods is None:
external_methods = {} external_methods = {}
external_methods.update(externalFuncs) external_methods.update(externalFuncs)
super(PythonScriptEngine).execute(task, script, external_methods) super().execute(task, script, external_methods)

View File

@ -1,21 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2020 Kelly McDonald, 2023 Sartography
import ast
import sys
import traceback
import warnings
from .PythonScriptEngineEnvironment import TaskDataEnvironment
from ..exceptions import SpiffWorkflowException, WorkflowTaskException
# Copyright (C) 2020 Kelly McDonald
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -25,6 +17,15 @@ from ..exceptions import SpiffWorkflowException, WorkflowTaskException
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import ast
import sys
import traceback
import warnings
from SpiffWorkflow.exceptions import SpiffWorkflowException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from .PythonScriptEngineEnvironment import TaskDataEnvironment
class PythonScriptEngine(object): class PythonScriptEngine(object):
""" """
@ -40,8 +41,8 @@ class PythonScriptEngine(object):
def __init__(self, default_globals=None, scripting_additions=None, environment=None): def __init__(self, default_globals=None, scripting_additions=None, environment=None):
if default_globals is not None or scripting_additions is not None: if default_globals is not None or scripting_additions is not None:
warnings.warn(f'default_globals and scripting_additions are deprecated. ' warnings.warn('default_globals and scripting_additions are deprecated. '
f'Please provide an environment such as TaskDataEnvrionment', 'Please provide an environment such as TaskDataEnvrionment',
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
if environment is None: if environment is None:

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import copy import copy
import warnings import warnings
@ -96,7 +115,7 @@ class Box(dict):
def __getattr__(self, attr): def __getattr__(self, attr):
try: try:
output = self[attr] output = self[attr]
except: except Exception:
raise AttributeError( raise AttributeError(
"Dictionary has no attribute '%s' " % str(attr)) "Dictionary has no attribute '%s' " % str(attr))
return output return output

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -14,4 +15,4 @@
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA

View File

@ -1,4 +1,90 @@
from SpiffWorkflow.exceptions import WorkflowTaskException # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import re
from SpiffWorkflow.util import levenshtein
from SpiffWorkflow.exceptions import WorkflowException
class WorkflowTaskException(WorkflowException):
"""WorkflowException that provides task_trace information."""
def __init__(self, error_msg, task=None, exception=None, line_number=None, offset=None, error_line=None):
"""
Exception initialization.
:param task: the task that threw the exception
:type task: Task
:param error_msg: a human readable error message
:type error_msg: str
:param exception: an exception to wrap, if any
:type exception: Exception
"""
self.task = task
self.line_number = line_number
self.offset = offset
self.error_line = error_line
if exception:
self.error_type = exception.__class__.__name__
else:
self.error_type = "unknown"
super().__init__(error_msg, task_spec=task.task_spec)
if isinstance(exception, SyntaxError) and not line_number:
# Line number and offset can be recovered directly from syntax errors,
# otherwise they must be passed in.
self.line_number = exception.lineno
self.offset = exception.offset
elif isinstance(exception, NameError):
self.add_note(self.did_you_mean_from_name_error(exception, list(task.data.keys())))
# If encountered in a sub-workflow, this traces back up the stack,
# so we can tell how we got to this particular task, no matter how
# deeply nested in sub-workflows it is. Takes the form of:
# task-description (file-name)
self.task_trace = self.get_task_trace(task)
@staticmethod
def get_task_trace(task):
task_trace = [f"{task.task_spec.bpmn_name} ({task.workflow.spec.file})"]
workflow = task.workflow
while workflow != workflow.outer_workflow:
caller = workflow.name
workflow = workflow.outer_workflow
task_trace.append(f"{workflow.spec.task_specs[caller].bpmn_name} ({workflow.spec.file})")
return task_trace
@staticmethod
def did_you_mean_from_name_error(name_exception, options):
"""Returns a string along the lines of 'did you mean 'dog'? Given
a name_error, and a set of possible things that could have been called,
or an empty string if no valid suggestions come up. """
def_match = re.match("name '(.+)' is not defined", str(name_exception))
if def_match:
bad_variable = re.match("name '(.+)' is not defined", str(name_exception)).group(1)
most_similar = levenshtein.most_similar(bad_variable, options, 3)
error_msg = ""
if len(most_similar) == 1:
error_msg += f' Did you mean \'{most_similar[0]}\'?'
if len(most_similar) > 1:
error_msg += f' Did you mean one of \'{most_similar}\'?'
return error_msg
class WorkflowDataException(WorkflowTaskException): class WorkflowDataException(WorkflowTaskException):

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -23,26 +23,36 @@ import os
from lxml import etree from lxml import etree
from lxml.etree import LxmlError from lxml.etree import LxmlError
from SpiffWorkflow.bpmn.specs.events.event_definitions import NoneEventDefinition from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
from SpiffWorkflow.bpmn.specs.defaults import (
UserTask,
ManualTask,
NoneTask,
ScriptTask,
ServiceTask,
CallActivity,
SubWorkflowTask,
TransactionSubprocess,
InclusiveGateway,
ExclusiveGateway,
ParallelGateway,
StartEvent,
EndEvent,
IntermediateCatchEvent,
IntermediateThrowEvent,
SendTask,
ReceiveTask,
BoundaryEvent,
EventBasedGateway
)
from SpiffWorkflow.bpmn.specs.event_definitions import NoneEventDefinition, TimerEventDefinition
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import SubWorkflowTask as SubWorkflowTaskMixin
from SpiffWorkflow.bpmn.specs.mixins.events.start_event import StartEvent as StartEventMixin
from .ValidationException import ValidationException from .ValidationException import ValidationException
from ..specs.BpmnProcessSpec import BpmnProcessSpec
from ..specs.data_spec import BpmnDataStoreSpecification
from ..specs.events.EndEvent import EndEvent
from ..specs.events.StartEvent import StartEvent
from ..specs.events.IntermediateEvent import BoundaryEvent, IntermediateCatchEvent, IntermediateThrowEvent, EventBasedGateway
from ..specs.events.IntermediateEvent import SendTask, ReceiveTask
from ..specs.SubWorkflowTask import CallActivity, SubWorkflowTask, TransactionSubprocess
from ..specs.ExclusiveGateway import ExclusiveGateway
from ..specs.InclusiveGateway import InclusiveGateway
from ..specs.ManualTask import ManualTask
from ..specs.NoneTask import NoneTask
from ..specs.ParallelGateway import ParallelGateway
from ..specs.ScriptTask import ScriptTask
from ..specs.ServiceTask import ServiceTask
from ..specs.UserTask import UserTask
from .ProcessParser import ProcessParser from .ProcessParser import ProcessParser
from .node_parser import DEFAULT_NSMAP from .node_parser import DEFAULT_NSMAP
from .spec_description import SPEC_DESCRIPTIONS
from .util import full_tag, xpath_eval, first from .util import full_tag, xpath_eval, first
from .TaskParser import TaskParser from .TaskParser import TaskParser
from .task_parsers import ( from .task_parsers import (
@ -86,7 +96,7 @@ class BpmnValidator:
except ValidationException as ve: except ValidationException as ve:
ve.file_name = filename ve.file_name = filename
ve.line_number = self.validator.error_log.last_error.line ve.line_number = self.validator.error_log.last_error.line
except LxmlError as le: except LxmlError:
last_error = self.validator.error_log.last_error last_error = self.validator.error_log.last_error
raise ValidationException(last_error.message, file_name=filename, raise ValidationException(last_error.message, file_name=filename,
line_number=last_error.line) line_number=last_error.line)
@ -132,14 +142,14 @@ class BpmnParser(object):
DATA_STORE_CLASSES = {} DATA_STORE_CLASSES = {}
def __init__(self, namespaces=None, validator=None): def __init__(self, namespaces=None, validator=None, spec_descriptions=SPEC_DESCRIPTIONS):
""" """
Constructor. Constructor.
""" """
self.namespaces = namespaces or DEFAULT_NSMAP self.namespaces = namespaces or DEFAULT_NSMAP
self.validator = validator self.validator = validator
self.spec_descriptions = spec_descriptions
self.process_parsers = {} self.process_parsers = {}
self.process_parsers_by_name = {}
self.collaborations = {} self.collaborations = {}
self.process_dependencies = set() self.process_dependencies = set()
self.messages = {} self.messages = {}
@ -153,15 +163,13 @@ class BpmnParser(object):
return self.PARSER_CLASSES[tag] return self.PARSER_CLASSES[tag]
return None, None return None, None
def get_process_parser(self, process_id_or_name): def get_process_parser(self, process_id):
""" """
Returns the ProcessParser for the given process ID or name. It matches Returns the ProcessParser for the given process ID or name. It matches
by name first. by name first.
""" """
if process_id_or_name in self.process_parsers_by_name: if process_id in self.process_parsers:
return self.process_parsers_by_name[process_id_or_name] return self.process_parsers[process_id]
elif process_id_or_name in self.process_parsers:
return self.process_parsers[process_id_or_name]
def get_process_ids(self): def get_process_ids(self):
"""Returns a list of process IDs""" """Returns a list of process IDs"""
@ -186,7 +194,19 @@ class BpmnParser(object):
""" """
for filename in filenames: for filename in filenames:
with open(filename, 'r') as f: with open(filename, 'r') as f:
self.add_bpmn_xml(etree.parse(f), filename=filename) self.add_bpmn_io(f, filename)
def add_bpmn_io(self, file_like_object, filename=None):
"""
Add the given BPMN file like object to the parser's set.
"""
self.add_bpmn_xml(etree.parse(file_like_object), filename)
def add_bpmn_str(self, bpmn_str, filename=None):
"""
Add the given BPMN string to the parser's set.
"""
self.add_bpmn_xml(etree.fromstring(bpmn_str), filename)
def add_bpmn_xml(self, bpmn, filename=None): def add_bpmn_xml(self, bpmn, filename=None):
""" """
@ -286,37 +306,37 @@ class BpmnParser(object):
def create_parser(self, node, filename=None, lane=None): def create_parser(self, node, filename=None, lane=None):
parser = self.PROCESS_PARSER_CLASS(self, node, self.namespaces, self.data_stores, filename=filename, lane=lane) parser = self.PROCESS_PARSER_CLASS(self, node, self.namespaces, self.data_stores, filename=filename, lane=lane)
if parser.get_id() in self.process_parsers: if parser.bpmn_id in self.process_parsers:
raise ValidationException(f'Duplicate process ID: {parser.get_id()}', node=node, file_name=filename) raise ValidationException(f'Duplicate process ID: {parser.bpmn_id}', node=node, file_name=filename)
if parser.get_name() in self.process_parsers_by_name: self.process_parsers[parser.bpmn_id] = parser
raise ValidationException(f'Duplicate process name: {parser.get_name()}', node=node, file_name=filename)
self.process_parsers[parser.get_id()] = parser
self.process_parsers_by_name[parser.get_name()] = parser
def get_process_dependencies(self): def get_process_dependencies(self):
return self.process_dependencies return self.process_dependencies
def get_spec(self, process_id_or_name): def get_spec(self, process_id, required=True):
""" """
Parses the required subset of the BPMN files, in order to provide an Parses the required subset of the BPMN files, in order to provide an
instance of BpmnProcessSpec (i.e. WorkflowSpec) instance of BpmnProcessSpec (i.e. WorkflowSpec)
for the given process ID or name. The Name is matched first. for the given process ID or name. The Name is matched first.
""" """
parser = self.get_process_parser(process_id_or_name) parser = self.get_process_parser(process_id)
if parser is None: if required and parser is None:
raise ValidationException( raise ValidationException(
f"The process '{process_id_or_name}' was not found. " f"The process '{process_id}' was not found. "
f"Did you mean one of the following: " f"Did you mean one of the following: "
f"{', '.join(self.get_process_ids())}?") f"{', '.join(self.get_process_ids())}?")
return parser.get_spec() elif parser is not None:
return parser.get_spec()
def get_subprocess_specs(self, name, specs=None): def get_subprocess_specs(self, name, specs=None, require_call_activity_specs=True):
used = specs or {} used = specs or {}
wf_spec = self.get_spec(name) wf_spec = self.get_spec(name)
for task_spec in wf_spec.task_specs.values(): for task_spec in wf_spec.task_specs.values():
if isinstance(task_spec, SubWorkflowTask) and task_spec.spec not in used: if isinstance(task_spec, SubWorkflowTaskMixin) and task_spec.spec not in used:
used[task_spec.spec] = self.get_spec(task_spec.spec) subprocess_spec = self.get_spec(task_spec.spec, required=require_call_activity_specs)
self.get_subprocess_specs(task_spec.spec, used) used[task_spec.spec] = subprocess_spec
if subprocess_spec is not None:
self.get_subprocess_specs(task_spec.spec, used)
return used return used
def find_all_specs(self): def find_all_specs(self):
@ -332,16 +352,24 @@ class BpmnParser(object):
self.find_all_specs() self.find_all_specs()
spec = BpmnProcessSpec(name) spec = BpmnProcessSpec(name)
subprocesses = {} subprocesses = {}
start = StartEvent(spec, 'Start Collaboration', NoneEventDefinition()) participant_type = self._get_parser_class(full_tag('callActivity'))[1]
start_type = self._get_parser_class(full_tag('startEvent'))[1]
end_type = self._get_parser_class(full_tag('endEvent'))[1]
start = start_type(spec, 'Start Collaboration', NoneEventDefinition())
spec.start.connect(start) spec.start.connect(start)
end = EndEvent(spec, 'End Collaboration', NoneEventDefinition()) end = end_type(spec, 'End Collaboration', NoneEventDefinition())
end.connect(spec.end) end.connect(spec.end)
for process in self.collaborations[name]: for process in self.collaborations[name]:
process_parser = self.get_process_parser(process) process_parser = self.get_process_parser(process)
if process_parser and process_parser.process_executable: if process_parser and process_parser.process_executable:
participant = CallActivity(spec, process, process) sp_spec = self.get_spec(process)
start.connect(participant) subprocesses[process] = sp_spec
participant.connect(end)
subprocesses[process] = self.get_spec(process)
subprocesses.update(self.get_subprocess_specs(process)) subprocesses.update(self.get_subprocess_specs(process))
if len([s for s in sp_spec.task_specs.values() if
isinstance(s, StartEventMixin) and
isinstance(s.event_definition, (NoneEventDefinition, TimerEventDefinition))
]):
participant = participant_type(spec, process, process)
start.connect(participant)
participant.connect(end)
return spec, subprocesses return spec, subprocesses

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -18,7 +18,7 @@
# 02110-1301 USA # 02110-1301 USA
from .ValidationException import ValidationException from .ValidationException import ValidationException
from ..specs.BpmnProcessSpec import BpmnProcessSpec from ..specs.bpmn_process_spec import BpmnProcessSpec
from ..specs.data_spec import DataObject from ..specs.data_spec import DataObject
from .node_parser import NodeParser from .node_parser import NodeParser
from .util import first from .util import first
@ -54,7 +54,7 @@ class ProcessParser(NodeParser):
""" """
Returns the process name (or ID, if no name is included in the file) Returns the process name (or ID, if no name is included in the file)
""" """
return self.node.get('name', default=self.get_id()) return self.node.get('name', default=self.bpmn_id)
def has_lanes(self) -> bool: def has_lanes(self) -> bool:
"""Returns true if this process has one or more named lanes """ """Returns true if this process has one or more named lanes """
@ -117,14 +117,14 @@ class ProcessParser(NodeParser):
start_node_list = self.xpath('./bpmn:startEvent') start_node_list = self.xpath('./bpmn:startEvent')
if not start_node_list and self.process_executable: if not start_node_list and self.process_executable:
raise ValidationException("No start event found", node=self.node, file_name=self.filename) raise ValidationException("No start event found", node=self.node, file_name=self.filename)
self.spec = BpmnProcessSpec(name=self.get_id(), description=self.get_name(), filename=self.filename) self.spec = BpmnProcessSpec(name=self.bpmn_id, description=self.get_name(), filename=self.filename)
self.spec.data_objects.update(self.inherited_data_objects) self.spec.data_objects.update(self.inherited_data_objects)
# Get the data objects # Get the data objects
for obj in self.xpath('./bpmn:dataObject'): for obj in self.xpath('./bpmn:dataObject'):
data_object = self.parse_data_object(obj) data_object = self.parse_data_object(obj)
self.spec.data_objects[data_object.name] = data_object self.spec.data_objects[data_object.bpmn_id] = data_object
# Check for an IO Specification. # Check for an IO Specification.
io_spec = first(self.xpath('./bpmn:ioSpecification')) io_spec = first(self.xpath('./bpmn:ioSpecification'))

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,17 +17,21 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .ValidationException import ValidationException from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import TransactionSubprocess
from ..specs.events.IntermediateEvent import _BoundaryEventParent from SpiffWorkflow.bpmn.specs.mixins.exclusive_gateway import ExclusiveGateway
from ..specs.events.event_definitions import CancelEventDefinition from SpiffWorkflow.bpmn.specs.mixins.inclusive_gateway import InclusiveGateway
from ..specs.MultiInstanceTask import StandardLoopTask, SequentialMultiInstanceTask, ParallelMultiInstanceTask from SpiffWorkflow.bpmn.specs.defaults import (
from ..specs.SubWorkflowTask import TransactionSubprocess StandardLoopTask,
from ..specs.ExclusiveGateway import ExclusiveGateway SequentialMultiInstanceTask,
from ..specs.InclusiveGateway import InclusiveGateway ParallelMultiInstanceTask
from ..specs.data_spec import TaskDataReference )
from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
from SpiffWorkflow.bpmn.specs.event_definitions import CancelEventDefinition
from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference
from .util import one from .util import one
from .node_parser import NodeParser from .node_parser import NodeParser
from .ValidationException import ValidationException
class TaskParser(NodeParser): class TaskParser(NodeParser):
@ -94,15 +98,17 @@ class TaskParser(NodeParser):
self._copy_task_attrs(original) self._copy_task_attrs(original)
def _add_multiinstance_task(self, loop_characteristics): def _add_multiinstance_task(self, loop_characteristics):
sequential = loop_characteristics.get('isSequential') == 'true' sequential = loop_characteristics.get('isSequential') == 'true'
prefix = 'bpmn:multiInstanceLoopCharacteristics' prefix = 'bpmn:multiInstanceLoopCharacteristics'
cardinality = self.xpath(f'./{prefix}/bpmn:loopCardinality') cardinality = self.xpath(f'./{prefix}/bpmn:loopCardinality')
loop_input = self.xpath(f'./{prefix}/bpmn:loopDataInputRef') loop_input = self.xpath(f'./{prefix}/bpmn:loopDataInputRef')
if len(cardinality) == 0 and len(loop_input) == 0: if len(cardinality) == 0 and len(loop_input) == 0:
self.raise_validation_exception("A multiinstance task must specify a cardinality or a loop input data reference") self.raise_validation_exception(
"A multiinstance task must specify a cardinality or a loop input data reference")
elif len(cardinality) > 0 and len(loop_input) > 0: elif len(cardinality) > 0 and len(loop_input) > 0:
self.raise_validation_exception("A multiinstance task must specify exactly one of cardinality or loop input data reference") self.raise_validation_exception(
"A multiinstance task must specify exactly one of cardinality or loop input data reference")
cardinality = cardinality[0].text if len(cardinality) > 0 else None cardinality = cardinality[0].text if len(cardinality) > 0 else None
loop_input = loop_input[0].text if len(loop_input) > 0 else None loop_input = loop_input[0].text if len(loop_input) > 0 else None
@ -110,7 +116,7 @@ class TaskParser(NodeParser):
if self.task.io_specification is not None: if self.task.io_specification is not None:
try: try:
loop_input = [v for v in self.task.io_specification.data_inputs if v.name == loop_input][0] loop_input = [v for v in self.task.io_specification.data_inputs if v.name == loop_input][0]
except: except Exception:
self.raise_validation_exception('The loop input data reference is missing from the IO specification') self.raise_validation_exception('The loop input data reference is missing from the IO specification')
else: else:
loop_input = TaskDataReference(loop_input) loop_input = TaskDataReference(loop_input)
@ -125,7 +131,7 @@ class TaskParser(NodeParser):
try: try:
refs = set(self.task.io_specification.data_inputs + self.task.io_specification.data_outputs) refs = set(self.task.io_specification.data_inputs + self.task.io_specification.data_outputs)
loop_output = [v for v in refs if v.name == loop_output][0] loop_output = [v for v in refs if v.name == loop_output][0]
except: except Exception:
self.raise_validation_exception('The loop output data reference is missing from the IO specification') self.raise_validation_exception('The loop output data reference is missing from the IO specification')
else: else:
loop_output = TaskDataReference(loop_output) loop_output = TaskDataReference(loop_output)
@ -138,8 +144,8 @@ class TaskParser(NodeParser):
original = self.spec.task_specs.pop(self.task.name) original = self.spec.task_specs.pop(self.task.name)
params = { params = {
'task_spec': '', 'task_spec': '',
'cardinality': cardinality, 'cardinality': cardinality,
'data_input': loop_input, 'data_input': loop_input,
'data_output':loop_output, 'data_output':loop_output,
'input_item': input_item, 'input_item': input_item,
@ -155,7 +161,7 @@ class TaskParser(NodeParser):
def _add_boundary_event(self, children): def _add_boundary_event(self, children):
parent = _BoundaryEventParent( parent = _BoundaryEventParent(
self.spec, '%s.BoundaryEventParent' % self.get_id(), self.spec, '%s.BoundaryEventParent' % self.bpmn_id,
self.task, lane=self.task.lane) self.task, lane=self.task.lane)
self.process_parser.parsed_nodes[self.node.get('id')] = parent self.process_parser.parsed_nodes[self.node.get('id')] = parent
parent.connect(self.task) parent.connect(self.task)
@ -176,10 +182,6 @@ class TaskParser(NodeParser):
# Why do we just set random attributes willy nilly everywhere in the code???? # Why do we just set random attributes willy nilly everywhere in the code????
# And we still pass around a gigantic kwargs dict whenever we create anything! # And we still pass around a gigantic kwargs dict whenever we create anything!
self.task.extensions = self.parse_extensions() self.task.extensions = self.parse_extensions()
self.task.documentation = self.parse_documentation()
# And now I have to add more of the same crappy thing.
self.task.data_input_associations = self.parse_incoming_data_references()
self.task.data_output_associations = self.parse_outgoing_data_references()
io_spec = self.xpath('./bpmn:ioSpecification') io_spec = self.xpath('./bpmn:ioSpecification')
if len(io_spec) > 0: if len(io_spec) > 0:
@ -193,30 +195,32 @@ class TaskParser(NodeParser):
if len(mi_loop_characteristics) > 0: if len(mi_loop_characteristics) > 0:
self._add_multiinstance_task(mi_loop_characteristics[0]) self._add_multiinstance_task(mi_loop_characteristics[0])
boundary_event_nodes = self.doc_xpath('.//bpmn:boundaryEvent[@attachedToRef="%s"]' % self.get_id()) boundary_event_nodes = self.doc_xpath('.//bpmn:boundaryEvent[@attachedToRef="%s"]' % self.bpmn_id)
if boundary_event_nodes: if boundary_event_nodes:
parent = self._add_boundary_event(boundary_event_nodes) parent = self._add_boundary_event(boundary_event_nodes)
else: else:
self.process_parser.parsed_nodes[self.node.get('id')] = self.task self.process_parser.parsed_nodes[self.node.get('id')] = self.task
children = [] children = []
outgoing = self.doc_xpath('.//bpmn:sequenceFlow[@sourceRef="%s"]' % self.get_id()) outgoing = self.doc_xpath('.//bpmn:sequenceFlow[@sourceRef="%s"]' % self.bpmn_id)
if len(outgoing) > 1 and not self.handles_multiple_outgoing(): if len(outgoing) > 1 and not self.handles_multiple_outgoing():
self.raise_validation_exception('Multiple outgoing flows are not supported for tasks of type') self.raise_validation_exception('Multiple outgoing flows are not supported for tasks of type')
for sequence_flow in outgoing: for sequence_flow in outgoing:
target_ref = sequence_flow.get('targetRef') target_ref = sequence_flow.get('targetRef')
try: try:
target_node = one(self.doc_xpath('.//bpmn:*[@id="%s"]'% target_ref)) target_node = one(self.doc_xpath('.//bpmn:*[@id="%s"]'% target_ref))
except: except Exception:
self.raise_validation_exception('When looking for a task spec, we found two items, ' self.raise_validation_exception('When looking for a task spec, we found two items, '
'perhaps a form has the same ID? (%s)' % target_ref) 'perhaps a form has the same ID? (%s)' % target_ref)
c = self.process_parser.parse_node(target_node) c = self.process_parser.parse_node(target_node)
position = c.position position = self.get_position(target_node)
children.append((position, c, target_node, sequence_flow)) children.append((position, c, target_node, sequence_flow))
if children: if children:
# Sort children by their y coordinate. # Sort children by their y coordinate.
# Why?? Isn't the point of parallel tasks that they can be executed in any order (or simultaneously)?
# And what if they're arranged horizontally?
children = sorted(children, key=lambda tup: float(tup[0]["y"])) children = sorted(children, key=lambda tup: float(tup[0]["y"]))
default_outgoing = self.node.get('default') default_outgoing = self.node.get('default')
@ -238,17 +242,14 @@ class TaskParser(NodeParser):
""" """
Returns a unique task spec name for this task (or the targeted one) Returns a unique task spec name for this task (or the targeted one)
""" """
return target_ref or self.get_id() return target_ref or self.bpmn_id
def create_task(self): def create_task(self):
""" """
Create an instance of the task appropriately. A subclass can override Create an instance of the task appropriately. A subclass can override
this method to get extra information from the node. this method to get extra information from the node.
""" """
return self.spec_class(self.spec, self.get_task_spec_name(), return self.spec_class(self.spec, self.bpmn_id, **self.bpmn_attributes)
lane=self.lane,
description=self.node.get('name', None),
position=self.position)
def connect_outgoing(self, outgoing_task, sequence_flow_node, is_default): def connect_outgoing(self, outgoing_task, sequence_flow_node, is_default):
""" """

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton, 2023 Dan Funk # Copyright (C) 2012 Matthew Hampton, 2023 Dan Funk
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -1,11 +1,28 @@
from lxml import etree # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.specs.events.event_definitions import CorrelationProperty from lxml import etree
from .ValidationException import ValidationException from .ValidationException import ValidationException
from .TaskParser import TaskParser from .TaskParser import TaskParser
from .util import first, one from .util import first, one
from ..specs.events.event_definitions import ( from ..specs.event_definitions import (
MultipleEventDefinition, MultipleEventDefinition,
TimeDateEventDefinition, TimeDateEventDefinition,
DurationTimerEventDefinition, DurationTimerEventDefinition,
@ -16,7 +33,8 @@ from ..specs.events.event_definitions import (
SignalEventDefinition, SignalEventDefinition,
CancelEventDefinition, CancelEventDefinition,
TerminateEventDefinition, TerminateEventDefinition,
NoneEventDefinition NoneEventDefinition,
CorrelationProperty
) )
CANCEL_EVENT_XPATH = './/bpmn:cancelEventDefinition' CANCEL_EVENT_XPATH = './/bpmn:cancelEventDefinition'
@ -31,8 +49,26 @@ TIMER_EVENT_XPATH = './/bpmn:timerEventDefinition'
class EventDefinitionParser(TaskParser): class EventDefinitionParser(TaskParser):
"""This class provvides methods for parsing different event definitions.""" """This class provvides methods for parsing different event definitions."""
def parse_cancel_event(self): def __init__(self, process_parser, spec_class, node, nsmap=None, lane=None):
return CancelEventDefinition() super().__init__(process_parser, spec_class, node, nsmap, lane)
self.event_nodes = []
def get_description(self):
spec_description = super().get_description()
if spec_description is not None:
if len(self.event_nodes) == 0:
event_description = 'Default'
elif len(self.event_nodes) > 1:
event_description = 'Multiple'
elif len(self.event_nodes) == 1:
event_description = self.process_parser.parser.spec_descriptions.get(self.event_nodes[0].tag)
return f'{event_description} {spec_description}'
def get_event_description(self, event):
return self.process_parser.parser.spec_descriptions.get(event.tag)
def parse_cancel_event(self, event):
return CancelEventDefinition(description=self.get_event_description(event))
def parse_error_event(self, error_event): def parse_error_event(self, error_event):
"""Parse the errorEventDefinition node and return an instance of ErrorEventDefinition.""" """Parse the errorEventDefinition node and return an instance of ErrorEventDefinition."""
@ -43,7 +79,7 @@ class EventDefinitionParser(TaskParser):
name = error.get('name') name = error.get('name')
else: else:
name, error_code = 'None Error Event', None name, error_code = 'None Error Event', None
return ErrorEventDefinition(name, error_code) return ErrorEventDefinition(name, error_code, description=self.get_event_description(error_event))
def parse_escalation_event(self, escalation_event): def parse_escalation_event(self, escalation_event):
"""Parse the escalationEventDefinition node and return an instance of EscalationEventDefinition.""" """Parse the escalationEventDefinition node and return an instance of EscalationEventDefinition."""
@ -55,7 +91,7 @@ class EventDefinitionParser(TaskParser):
name = escalation.get('name') name = escalation.get('name')
else: else:
name, escalation_code = 'None Escalation Event', None name, escalation_code = 'None Escalation Event', None
return EscalationEventDefinition(name, escalation_code) return EscalationEventDefinition(name, escalation_code, description=self.get_event_description(escalation_event))
def parse_message_event(self, message_event): def parse_message_event(self, message_event):
@ -63,11 +99,13 @@ class EventDefinitionParser(TaskParser):
if message_ref is not None: if message_ref is not None:
message = one(self.doc_xpath('.//bpmn:message[@id="%s"]' % message_ref)) message = one(self.doc_xpath('.//bpmn:message[@id="%s"]' % message_ref))
name = message.get('name') name = message.get('name')
description = self.get_event_description(message_event)
correlations = self.get_message_correlations(message_ref) correlations = self.get_message_correlations(message_ref)
else: else:
name = message_event.getparent().get('name') name = message_event.getparent().get('name')
description = 'Message'
correlations = {} correlations = {}
return MessageEventDefinition(name, correlations) return MessageEventDefinition(name, correlations, description=description)
def parse_signal_event(self, signal_event): def parse_signal_event(self, signal_event):
"""Parse the signalEventDefinition node and return an instance of SignalEventDefinition.""" """Parse the signalEventDefinition node and return an instance of SignalEventDefinition."""
@ -78,26 +116,26 @@ class EventDefinitionParser(TaskParser):
name = signal.get('name') name = signal.get('name')
else: else:
name = signal_event.getparent().get('name') name = signal_event.getparent().get('name')
return SignalEventDefinition(name) return SignalEventDefinition(name, description=self.get_event_description(signal_event))
def parse_terminate_event(self): def parse_terminate_event(self, event):
"""Parse the terminateEventDefinition node and return an instance of TerminateEventDefinition.""" """Parse the terminateEventDefinition node and return an instance of TerminateEventDefinition."""
return TerminateEventDefinition() return TerminateEventDefinition(description=self.get_event_description(event))
def parse_timer_event(self): def parse_timer_event(self, event):
"""Parse the timerEventDefinition node and return an instance of TimerEventDefinition.""" """Parse the timerEventDefinition node and return an instance of TimerEventDefinition."""
try: try:
description = self.get_event_description(event)
name = self.node.get('name', self.node.get('id')) name = self.node.get('name', self.node.get('id'))
time_date = first(self.xpath('.//bpmn:timeDate')) time_date = first(self.xpath('.//bpmn:timeDate'))
if time_date is not None: if time_date is not None:
return TimeDateEventDefinition(name, time_date.text) return TimeDateEventDefinition(name, time_date.text, description=description)
time_duration = first(self.xpath('.//bpmn:timeDuration')) time_duration = first(self.xpath('.//bpmn:timeDuration'))
if time_duration is not None: if time_duration is not None:
return DurationTimerEventDefinition(name, time_duration.text) return DurationTimerEventDefinition(name, time_duration.text, description=description)
time_cycle = first(self.xpath('.//bpmn:timeCycle')) time_cycle = first(self.xpath('.//bpmn:timeCycle'))
if time_cycle is not None: if time_cycle is not None:
return CycleTimerEventDefinition(name, time_cycle.text) return CycleTimerEventDefinition(name, time_cycle.text, description=description)
raise ValidationException("Unknown Time Specification", node=self.node, file_name=self.filename) raise ValidationException("Unknown Time Specification", node=self.node, file_name=self.filename)
except Exception as e: except Exception as e:
raise ValidationException("Time Specification Error. " + str(e), node=self.node, file_name=self.filename) raise ValidationException("Time Specification Error. " + str(e), node=self.node, file_name=self.filename)
@ -125,16 +163,14 @@ class EventDefinitionParser(TaskParser):
if prop.name not in self.spec.correlation_keys[key]: if prop.name not in self.spec.correlation_keys[key]:
self.spec.correlation_keys[key].append(prop.name) self.spec.correlation_keys[key].append(prop.name)
kwargs = { kwargs = self.bpmn_attributes
'lane': self.lane,
'description': self.node.get('name', None),
'position': self.position,
}
if cancel_activity is not None: if cancel_activity is not None:
kwargs['cancel_activity'] = cancel_activity kwargs['cancel_activity'] = cancel_activity
interrupt = 'Interrupting' if cancel_activity else 'Non-Interrupting'
kwargs['description'] = interrupt + ' ' + kwargs['description']
if parallel is not None: if parallel is not None:
kwargs['parallel'] = parallel kwargs['parallel'] = parallel
return self.spec_class(self.spec, self.get_task_spec_name(), event_definition, **kwargs) return self.spec_class(self.spec, self.bpmn_id, event_definition=event_definition, **kwargs)
def get_event_definition(self, xpaths): def get_event_definition(self, xpaths):
"""Returns all event definitions it can find in given list of xpaths""" """Returns all event definitions it can find in given list of xpaths"""
@ -142,29 +178,31 @@ class EventDefinitionParser(TaskParser):
event_definitions = [] event_definitions = []
for path in xpaths: for path in xpaths:
for event in self.xpath(path): for event in self.xpath(path):
if event is not None:
self.event_nodes.append(event)
if path == MESSAGE_EVENT_XPATH: if path == MESSAGE_EVENT_XPATH:
event_definitions.append(self.parse_message_event(event)) event_definitions.append(self.parse_message_event(event))
elif path == SIGNAL_EVENT_XPATH: elif path == SIGNAL_EVENT_XPATH:
event_definitions.append(self.parse_signal_event(event)) event_definitions.append(self.parse_signal_event(event))
elif path == TIMER_EVENT_XPATH: elif path == TIMER_EVENT_XPATH:
event_definitions.append(self.parse_timer_event()) event_definitions.append(self.parse_timer_event(event))
elif path == CANCEL_EVENT_XPATH: elif path == CANCEL_EVENT_XPATH:
event_definitions.append(self.parse_cancel_event()) event_definitions.append(self.parse_cancel_event(event))
elif path == ERROR_EVENT_XPATH: elif path == ERROR_EVENT_XPATH:
event_definitions.append(self.parse_error_event(event)) event_definitions.append(self.parse_error_event(event))
elif path == ESCALATION_EVENT_XPATH: elif path == ESCALATION_EVENT_XPATH:
event_definitions.append(self.parse_escalation_event(event)) event_definitions.append(self.parse_escalation_event(event))
elif path == TERMINATION_EVENT_XPATH: elif path == TERMINATION_EVENT_XPATH:
event_definitions.append(self.parse_terminate_event()) event_definitions.append(self.parse_terminate_event(event))
parallel = self.node.get('parallelMultiple') == 'true' parallel = self.node.get('parallelMultiple') == 'true'
if len(event_definitions) == 0: if len(event_definitions) == 0:
return NoneEventDefinition() return NoneEventDefinition(description='Default')
elif len(event_definitions) == 1: elif len(event_definitions) == 1:
return event_definitions[0] return event_definitions[0]
else: else:
return MultipleEventDefinition(event_definitions, parallel) return MultipleEventDefinition(event_definitions, parallel, description='Multiple')
class StartEventParser(EventDefinitionParser): class StartEventParser(EventDefinitionParser):

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference, BpmnIoSpecification from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference, BpmnIoSpecification
from .util import first from .util import first
@ -17,11 +36,25 @@ class NodeParser:
self.nsmap = nsmap or DEFAULT_NSMAP self.nsmap = nsmap or DEFAULT_NSMAP
self.filename = filename self.filename = filename
self.lane = self._get_lane() or lane self.lane = self._get_lane() or lane
self.position = self._get_position() or {'x': 0.0, 'y': 0.0}
def get_id(self): @property
def bpmn_id(self):
return self.node.get('id') return self.node.get('id')
@property
def bpmn_attributes(self):
return {
'description': self.get_description(),
'lane': self.lane,
'bpmn_name': self.node.get('name'),
'documentation': self.parse_documentation(),
'data_input_associations': self.parse_incoming_data_references(),
'data_output_associations': self.parse_outgoing_data_references(),
}
def get_description(self):
return self.process_parser.parser.spec_descriptions.get(self.node.tag)
def xpath(self, xpath, extra_ns=None): def xpath(self, xpath, extra_ns=None):
return self._xpath(self.node, xpath, extra_ns) return self._xpath(self.node, xpath, extra_ns)
@ -76,10 +109,10 @@ class NodeParser:
data_refs = {} data_refs = {}
for elem in self.xpath('./bpmn:ioSpecification/bpmn:dataInput'): for elem in self.xpath('./bpmn:ioSpecification/bpmn:dataInput'):
ref = self.create_data_spec(elem, TaskDataReference) ref = self.create_data_spec(elem, TaskDataReference)
data_refs[ref.name] = ref data_refs[ref.bpmn_id] = ref
for elem in self.xpath('./bpmn:ioSpecification/bpmn:dataOutput'): for elem in self.xpath('./bpmn:ioSpecification/bpmn:dataOutput'):
ref = self.create_data_spec(elem, TaskDataReference) ref = self.create_data_spec(elem, TaskDataReference)
data_refs[ref.name] = ref data_refs[ref.bpmn_id] = ref
inputs, outputs = [], [] inputs, outputs = [], []
for ref in self.xpath('./bpmn:ioSpecification/bpmn:inputSet/bpmn:dataInputRefs'): for ref in self.xpath('./bpmn:ioSpecification/bpmn:inputSet/bpmn:dataInputRefs'):
@ -96,16 +129,20 @@ class NodeParser:
def parse_extensions(self, node=None): def parse_extensions(self, node=None):
return {} return {}
def get_position(self, node=None):
node = node if node is not None else self.node
nodeid = node.get('id')
if nodeid is not None:
bounds = first(self.doc_xpath(f".//bpmndi:BPMNShape[@bpmnElement='{nodeid}']//dc:Bounds"))
if bounds is not None:
return {'x': float(bounds.get('x', 0)), 'y': float(bounds.get('y', 0))}
return {'x': 0.0, 'y': 0.0}
def _get_lane(self): def _get_lane(self):
noderef = first(self.doc_xpath(f".//bpmn:flowNodeRef[text()='{self.get_id()}']")) noderef = first(self.doc_xpath(f".//bpmn:flowNodeRef[text()='{self.bpmn_id}']"))
if noderef is not None: if noderef is not None:
return noderef.getparent().get('name') return noderef.getparent().get('name')
def _get_position(self):
bounds = first(self.doc_xpath(f".//bpmndi:BPMNShape[@bpmnElement='{self.get_id()}']//dc:Bounds"))
if bounds is not None:
return {'x': float(bounds.get('x', 0)), 'y': float(bounds.get('y', 0))}
def _xpath(self, node, xpath, extra_ns=None): def _xpath(self, node, xpath, extra_ns=None):
if extra_ns is not None: if extra_ns is not None:
nsmap = self.nsmap.copy() nsmap = self.nsmap.copy()

View File

@ -0,0 +1,52 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .util import full_tag
# Having this configurable via the parser makes a lot more sense than requiring a subclass
# This can be further streamlined if we ever replace our parser
SPEC_DESCRIPTIONS = {
full_tag('startEvent'): 'Start Event',
full_tag('endEvent'): 'End Event',
full_tag('userTask'): 'User Task',
full_tag('task'): 'Task',
full_tag('subProcess'): 'Subprocess',
full_tag('manualTask'): 'Manual Task',
full_tag('exclusiveGateway'): 'Exclusive Gateway',
full_tag('parallelGateway'): 'Parallel Gateway',
full_tag('inclusiveGateway'): 'Inclusive Gateway',
full_tag('callActivity'): 'Call Activity',
full_tag('transaction'): 'Transaction',
full_tag('scriptTask'): 'Script Task',
full_tag('serviceTask'): 'Service Task',
full_tag('intermediateCatchEvent'): 'Intermediate Catch Event',
full_tag('intermediateThrowEvent'): 'Intermediate Throw Event',
full_tag('boundaryEvent'): 'Boundary Event',
full_tag('receiveTask'): 'Receive Task',
full_tag('sendTask'): 'Send Task',
full_tag('eventBasedGateway'): 'Event Based Gateway',
full_tag('cancelEventDefinition'): 'Cancel',
full_tag('errorEventDefinition'): 'Error',
full_tag('escalationEventDefinition'): 'Escalation',
full_tag('terminateEventDefinition'): 'Terminate',
full_tag('messageEventDefinition'): 'Message',
full_tag('signalEventDefinition'): 'Signal',
full_tag('timerEventDefinition'): 'Timer',
}

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -85,13 +86,6 @@ class SubprocessParser:
'No "calledElement" attribute for Call Activity.', 'No "calledElement" attribute for Call Activity.',
node=task_parser.node, node=task_parser.node,
file_name=task_parser.filename) file_name=task_parser.filename)
parser = task_parser.process_parser.parser.get_process_parser(called_element)
if parser is None:
raise ValidationException(
f"The process '{called_element}' was not found. Did you mean one of the following: "
f"{', '.join(task_parser.process_parser.parser.get_process_ids())}?",
node=task_parser.node,
file_name=task_parser.filename)
return called_element return called_element
@ -99,10 +93,7 @@ class SubWorkflowParser(TaskParser):
def create_task(self): def create_task(self):
subworkflow_spec = SubprocessParser.get_subprocess_spec(self) subworkflow_spec = SubprocessParser.get_subprocess_spec(self)
return self.spec_class( return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
class CallActivityParser(TaskParser): class CallActivityParser(TaskParser):
@ -110,23 +101,14 @@ class CallActivityParser(TaskParser):
def create_task(self): def create_task(self):
subworkflow_spec = SubprocessParser.get_call_activity_spec(self) subworkflow_spec = SubprocessParser.get_call_activity_spec(self)
return self.spec_class( return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
class ScriptTaskParser(TaskParser): class ScriptTaskParser(TaskParser):
""" """Parses a script task"""
Parses a script task
"""
def create_task(self): def create_task(self):
script = self.get_script() return self.spec_class(self.spec, self.bpmn_id, script=self.get_script(), **self.bpmn_attributes)
return self.spec_class(self.spec, self.get_task_spec_name(), script,
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
def get_script(self): def get_script(self):
""" """
@ -138,6 +120,6 @@ class ScriptTaskParser(TaskParser):
return one(self.xpath('.//bpmn:script')).text return one(self.xpath('.//bpmn:script')).text
except AssertionError as ae: except AssertionError as ae:
raise ValidationException( raise ValidationException(
f"Invalid Script Task. No Script Provided. " + str(ae), "Invalid Script Task. No Script Provided. " + str(ae),
node=self.node, file_name=self.filename) node=self.node, file_name=self.filename)

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ..specs.data_spec import DataObject, TaskDataReference, BpmnIoSpecification from ..specs.data_spec import DataObject, TaskDataReference, BpmnIoSpecification
from .helpers.spec import BpmnSpecConverter, BpmnDataSpecificationConverter from .helpers.spec import BpmnSpecConverter, BpmnDataSpecificationConverter

View File

@ -1,6 +1,23 @@
from .helpers.spec import EventDefinitionConverter # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ..specs.events.event_definitions import ( from SpiffWorkflow.bpmn.specs.event_definitions import (
CancelEventDefinition, CancelEventDefinition,
ErrorEventDefinition, ErrorEventDefinition,
EscalationEventDefinition, EscalationEventDefinition,
@ -13,6 +30,7 @@ from ..specs.events.event_definitions import (
CycleTimerEventDefinition, CycleTimerEventDefinition,
MultipleEventDefinition, MultipleEventDefinition,
) )
from .helpers.spec import EventDefinitionConverter
class CancelEventDefinitionConverter(EventDefinitionConverter): class CancelEventDefinitionConverter(EventDefinitionConverter):
def __init__(self, registry): def __init__(self, registry):

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Elizabeth Esswein, Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from functools import partial from functools import partial
class DictionaryConverter: class DictionaryConverter:

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Elizabeth Esswein, Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from uuid import UUID from uuid import UUID
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@ -1,9 +1,32 @@
# Copyright (C) 2023 Elizabeth Esswein, Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from functools import partial from functools import partial
from ...specs.BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.operators import Attrib, PathAttrib
from ...specs.events.event_definitions import NamedEventDefinition, TimerEventDefinition
from ...specs.events.event_definitions import CorrelationProperty from SpiffWorkflow.bpmn.specs.mixins.bpmn_spec_mixin import BpmnSpecMixin
from ....operators import Attrib, PathAttrib from SpiffWorkflow.bpmn.specs.event_definitions import (
NamedEventDefinition,
TimerEventDefinition,
CorrelationProperty
)
class BpmnSpecConverter: class BpmnSpecConverter:
@ -57,7 +80,7 @@ class BpmnDataSpecificationConverter(BpmnSpecConverter):
""" """
def to_dict(self, data_spec): def to_dict(self, data_spec):
return { 'name': data_spec.name, 'description': data_spec.description } return { 'bpmn_id': data_spec.bpmn_id, 'bpmn_name': data_spec.bpmn_name }
def from_dict(self, dct): def from_dict(self, dct):
return self.spec_class(**dct) return self.spec_class(**dct)
@ -72,7 +95,11 @@ class EventDefinitionConverter(BpmnSpecConverter):
""" """
def to_dict(self, event_definition): def to_dict(self, event_definition):
dct = {'internal': event_definition.internal, 'external': event_definition.external} dct = {
'internal': event_definition.internal,
'external': event_definition.external,
'description': event_definition.description,
}
if isinstance(event_definition, (NamedEventDefinition, TimerEventDefinition)): if isinstance(event_definition, (NamedEventDefinition, TimerEventDefinition)):
dct['name'] = event_definition.name dct['name'] = event_definition.name
return dct return dct
@ -105,7 +132,7 @@ class TaskSpecConverter(BpmnSpecConverter):
modules of this package; the `camunda`,`dmn`, and `spiff` serialization packages contain other modules of this package; the `camunda`,`dmn`, and `spiff` serialization packages contain other
examples. examples.
""" """
def get_default_attributes(self, spec, include_data=False): def get_default_attributes(self, spec):
"""Extracts the default Spiff attributes from a task spec. """Extracts the default Spiff attributes from a task spec.
:param spec: the task spec to be converted :param spec: the task spec to be converted
@ -113,38 +140,17 @@ class TaskSpecConverter(BpmnSpecConverter):
Returns: Returns:
a dictionary of standard task spec attributes a dictionary of standard task spec attributes
""" """
dct = { return {
'id': spec.id,
'name': spec.name, 'name': spec.name,
'description': spec.description, 'description': spec.description,
'manual': spec.manual, 'manual': spec.manual,
'internal': spec.internal,
'lookahead': spec.lookahead, 'lookahead': spec.lookahead,
'inputs': [task.name for task in spec.inputs], 'inputs': [task.name for task in spec.inputs],
'outputs': [task.name for task in spec.outputs], 'outputs': [task.name for task in spec.outputs],
} 'bpmn_id': spec.bpmn_id,
# This stuff is also all defined in the base task spec, but can contain data, so we need 'bpmn_name': spec.bpmn_name,
# our data serializer. I think we should try to get this stuff out of the base task spec.
if include_data:
dct['data'] = self.registry.convert(spec.data)
dct['defines'] = self.registry.convert(spec.defines)
dct['pre_assign'] = self.registry.convert(spec.pre_assign)
dct['post_assign'] = self.registry.convert(spec.post_assign)
return dct
def get_bpmn_attributes(self, spec):
"""Extracts the attributes added by the `BpmnSpecMixin` class.
:param spec: the task spec to be converted
Returns:
a dictionary of BPMN task spec attributes
"""
return {
'lane': spec.lane, 'lane': spec.lane,
'documentation': spec.documentation, 'documentation': spec.documentation,
'position': spec.position,
'data_input_associations': [ self.registry.convert(obj) for obj in spec.data_input_associations ], 'data_input_associations': [ self.registry.convert(obj) for obj in spec.data_input_associations ],
'data_output_associations': [ self.registry.convert(obj) for obj in spec.data_output_associations ], 'data_output_associations': [ self.registry.convert(obj) for obj in spec.data_output_associations ],
'io_specification': self.registry.convert(spec.io_specification), 'io_specification': self.registry.convert(spec.io_specification),
@ -189,38 +195,36 @@ class TaskSpecConverter(BpmnSpecConverter):
'test_before': spec.test_before, 'test_before': spec.test_before,
} }
def task_spec_from_dict(self, dct, include_data=False): def task_spec_from_dict(self, dct):
""" """
Creates a task spec based on the supplied dictionary. It handles setting the default Creates a task spec based on the supplied dictionary. It handles setting the default
task spec attributes as well as attributes added by `BpmnSpecMixin`. task spec attributes as well as attributes added by `BpmnSpecMixin`.
:param dct: the dictionary to create the task spec from :param dct: the dictionary to create the task spec from
:param include_data: whether or not to include task spec data attributes
Returns: Returns:
a restored task spec a restored task spec
""" """
internal = dct.pop('internal') dct['data_input_associations'] = self.registry.restore(dct.pop('data_input_associations', []))
dct['data_output_associations'] = self.registry.restore(dct.pop('data_output_associations', []))
inputs = dct.pop('inputs') inputs = dct.pop('inputs')
outputs = dct.pop('outputs') outputs = dct.pop('outputs')
spec = self.spec_class(**dct) wf_spec = dct.pop('wf_spec')
spec.internal = internal name = dct.pop('name')
bpmn_id = dct.pop('bpmn_id')
spec = self.spec_class(wf_spec, name, **dct)
spec.inputs = inputs spec.inputs = inputs
spec.outputs = outputs spec.outputs = outputs
spec.id = dct['id']
if include_data: if issubclass(self.spec_class, BpmnSpecMixin) and bpmn_id != name:
spec.data = self.registry.restore(dct.get('data', {})) # This is a hack for multiinstance tasks :( At least it is simple.
spec.defines = self.registry.restore(dct.get('defines', {})) # Ideally I'd fix it in the parser, but I'm afraid of quickly running into a wall there
spec.pre_assign = self.registry.restore(dct.get('pre_assign', {})) spec.bpmn_id = bpmn_id
spec.post_assign = self.registry.restore(dct.get('post_assign', {}))
if isinstance(spec, BpmnSpecMixin): if isinstance(spec, BpmnSpecMixin):
spec.documentation = dct.pop('documentation', None)
spec.lane = dct.pop('lane', None)
spec.data_input_associations = self.registry.restore(dct.pop('data_input_associations', []))
spec.data_output_associations = self.registry.restore(dct.pop('data_output_associations', []))
spec.io_specification = self.registry.restore(dct.pop('io_specification', None)) spec.io_specification = self.registry.restore(dct.pop('io_specification', None))
return spec return spec

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.exceptions import WorkflowException from SpiffWorkflow.exceptions import WorkflowException
class VersionMigrationError(WorkflowException): class VersionMigrationError(WorkflowException):

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
def move_subprocesses_to_top(dct): def move_subprocesses_to_top(dct):
subprocesses = dict((sp, { 'tasks': {}, 'root': None, 'data': {}, 'success': True }) for sp in dct['subprocesses']) subprocesses = dict((sp, { 'tasks': {}, 'root': None, 'data': {}, 'success': True }) for sp in dct['subprocesses'])

View File

@ -1,7 +1,26 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from datetime import datetime, timedelta from datetime import datetime, timedelta
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.specs.events.event_definitions import LOCALTZ from SpiffWorkflow.bpmn.specs.event_definitions import LOCALTZ
from .exceptions import VersionMigrationError from .exceptions import VersionMigrationError
@ -29,7 +48,7 @@ def convert_timer_expressions(dct):
elif isinstance(dt, timedelta): elif isinstance(dt, timedelta):
spec['event_definition']['expression'] = f"'{td_to_iso(dt)}'" spec['event_definition']['expression'] = f"'{td_to_iso(dt)}'"
spec['event_definition']['typename'] = 'DurationTimerEventDefinition' spec['event_definition']['typename'] = 'DurationTimerEventDefinition'
except: except Exception:
raise VersionMigrationError(message.format(spec=spec['name'])) raise VersionMigrationError(message.format(spec=spec['name']))
def convert_cycle(spec, task): def convert_cycle(spec, task):
@ -47,7 +66,7 @@ def convert_timer_expressions(dct):
'next': datetime.combine(dt.date(), dt.time(), LOCALTZ).isoformat(), 'next': datetime.combine(dt.date(), dt.time(), LOCALTZ).isoformat(),
'duration': duration.total_seconds(), 'duration': duration.total_seconds(),
} }
except: except Exception:
raise VersionMigrationError(message.format(spec=spec['name'])) raise VersionMigrationError(message.format(spec=spec['name']))
if spec['typename'] == 'StartEvent': if spec['typename'] == 'StartEvent':
@ -65,7 +84,8 @@ def convert_timer_expressions(dct):
task['children'].remove(remove['id']) task['children'].remove(remove['id'])
dct['tasks'].pop(remove['id']) dct['tasks'].pop(remove['id'])
has_timer = lambda ts: 'event_definition' in ts and ts['event_definition']['typename'] in [ 'CycleTimerEventDefinition', 'TimerEventDefinition'] def has_timer(ts):
return "event_definition" in ts and ts["event_definition"]["typename"] in ["CycleTimerEventDefinition", "TimerEventDefinition"]
for spec in [ ts for ts in dct['spec']['task_specs'].values() if has_timer(ts) ]: for spec in [ ts for ts in dct['spec']['task_specs'].values() if has_timer(ts) ]:
spec['event_definition']['name'] = spec['event_definition'].pop('label') spec['event_definition']['name'] = spec['event_definition'].pop('label')
if spec['event_definition']['typename'] == 'TimerEventDefinition': if spec['event_definition']['typename'] == 'TimerEventDefinition':
@ -113,7 +133,7 @@ def create_data_objects_and_io_specs(dct):
item['typename'] = 'DataObject' item['typename'] = 'DataObject'
def check_multiinstance(dct): def check_multiinstance(dct):
specs = [ spec for spec in dct['spec']['task_specs'].values() if 'prevtaskclass' in spec ] specs = [ spec for spec in dct['spec']['task_specs'].values() if 'prevtaskclass' in spec ]
if len(specs) > 0: if len(specs) > 0:
raise VersionMigrationError("This workflow cannot be migrated because it contains MultiInstance Tasks") raise VersionMigrationError("This workflow cannot be migrated because it contains MultiInstance Tasks")
@ -143,3 +163,97 @@ def update_task_states(dct):
update(dct) update(dct)
for sp in dct['subprocesses'].values(): for sp in dct['subprocesses'].values():
update(sp) update(sp)
def convert_simple_tasks(dct):
def update_specs(task_specs):
for name, spec in task_specs.items():
if spec['typename'] == 'StartTask':
spec['typename'] = 'BpmnStartTask'
elif spec['typename'] == 'Simple':
spec['typename'] = 'SimpleBpmnTask'
update_specs(dct['spec']['task_specs'])
for subprocess_spec in dct['subprocess_specs'].values():
update_specs(subprocess_spec['task_specs'])
def update_bpmn_attributes(dct):
descriptions = {
'StartEvent': 'Start Event',
'EndEvent': 'End Event',
'UserTask': 'User Task',
'Task': 'Task',
'SubProcess': 'Subprocess',
'ManualTask': 'Manual Task',
'ExclusiveGateway': 'Exclusive Gateway',
'ParallelGateway': 'Parallel Gateway',
'InclusiveGateway': 'Inclusive Gateway',
'CallActivity': 'Call Activity',
'TransactionSubprocess': 'Transaction',
'ScriptTask': 'Script Task',
'ServiceTask': 'Service Task',
'IntermediateCatchEvent': 'Intermediate Catch Event',
'IntermediateThrowEvent': 'Intermediate Throw Event',
'BoundaryEvent': 'Boundary Event',
'ReceiveTask': 'Receive Task',
'SendTask': 'Send Task',
'EventBasedGateway': 'Event Based Gateway',
'CancelEventDefinition': 'Cancel',
'ErrorEventDefinition': 'Error',
'EscalationEventDefinition': 'Escalation',
'TerminateEventDefinition': 'Terminate',
'MessageEventDefinition': 'Message',
'SignalEventDefinition': 'Signal',
'TimerEventDefinition': 'Timer',
'NoneEventDefinition': 'Default',
'MultipleEventDefinition': 'Multiple'
}
def update_data_spec(obj):
obj['bpmn_id'] = obj.pop('name')
obj['bpmn_name'] = obj.pop('description', None)
def update_io_spec(io_spec):
for obj in io_spec['data_inputs']:
update_data_spec(obj)
for obj in io_spec['data_outputs']:
update_data_spec(obj)
def update_task_specs(spec):
for spec in spec['task_specs'].values():
spec['bpmn_id'] = None
if spec['typename'] not in ['BpmnStartTask', 'SimpleBpmnTask', '_EndJoin', '_BoundaryEventParent']:
spec['bpmn_id'] = spec['name']
spec['bpmn_name'] = spec['description'] or None
if 'event_definition' in spec and spec['event_definition']['typename'] in descriptions:
spec_desc = descriptions.get(spec['typename'])
event_desc = descriptions.get(spec['event_definition']['typename'])
cancelling = spec.get('cancel_activity')
interrupt = 'Interrupting ' if cancelling else 'Non-Interrupting ' if not cancelling else ''
desc = f'{interrupt}{event_desc} {spec_desc}'
elif spec['typename'] in descriptions:
desc = descriptions.get(spec['typename'])
else:
desc = None
spec['description'] = desc
else:
spec['bpmn_name'] = None
spec['description'] = None
if spec.get('io_specification') is not None:
update_io_spec(spec['io_specification'])
for obj in spec.get('data_input_associations', []):
update_data_spec(obj)
for obj in spec.get('data_output_associations', []):
update_data_spec(obj)
update_task_specs(dct['spec'])
for obj in dct['spec'].get('data_objects', {}).values():
update_data_spec(obj)
for subprocess_spec in dct['subprocess_specs'].values():
update_task_specs(subprocess_spec)
for obj in subprocess_spec.get('data_objects', {}).values():
update_data_spec(obj)
if subprocess_spec.get('io_specification') is not None:
update_io_spec(subprocess_spec['io_specification'])

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from copy import deepcopy from copy import deepcopy
from .version_1_1 import move_subprocesses_to_top from .version_1_1 import move_subprocesses_to_top
@ -8,6 +27,8 @@ from .version_1_2 import (
check_multiinstance, check_multiinstance,
remove_loop_reset, remove_loop_reset,
update_task_states, update_task_states,
convert_simple_tasks,
update_bpmn_attributes,
) )
def from_version_1_1(old): def from_version_1_1(old):
@ -38,6 +59,8 @@ def from_version_1_1(old):
check_multiinstance(new) check_multiinstance(new)
remove_loop_reset(new) remove_loop_reset(new)
update_task_states(new) update_task_states(new)
convert_simple_tasks(new)
update_bpmn_attributes(new)
new['VERSION'] = "1.2" new['VERSION'] = "1.2"
return new return new

View File

@ -1,5 +1,24 @@
from ..specs.BpmnProcessSpec import BpmnProcessSpec # Copyright (C) 2023 Sartography
from ..specs.events.IntermediateEvent import _BoundaryEventParent #
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.specs.bpmn_process_spec import BpmnProcessSpec
from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
from .helpers.spec import WorkflowSpecConverter from .helpers.spec import WorkflowSpecConverter
@ -39,12 +58,9 @@ class BpmnProcessSpecConverter(WorkflowSpecConverter):
def from_dict(self, dct): def from_dict(self, dct):
spec = self.spec_class(name=dct['name'], description=dct['description'], filename=dct['file']) spec = self.spec_class(name=dct['name'], description=dct['description'], filename=dct['file'])
# There a nostart arg in the base workflow spec class that prevents start task creation, but # These are automatically created with a workflow and should be replaced
# the BPMN process spec doesn't pass it in, so we have to delete the auto generated Start task.
del spec.task_specs['Start'] del spec.task_specs['Start']
spec.start = None spec.start = None
# These are also automatically created with a workflow and should be replaced
del spec.task_specs['End'] del spec.task_specs['End']
del spec.task_specs[f'{spec.name}.EndJoin'] del spec.task_specs[f'{spec.name}.EndJoin']
@ -60,6 +76,7 @@ class BpmnProcessSpecConverter(WorkflowSpecConverter):
# Add messaging related stuff # Add messaging related stuff
spec.correlation_keys = dct.pop('correlation_keys', {}) spec.correlation_keys = dct.pop('correlation_keys', {})
dct['task_specs'].pop('Root', None)
for name, task_dict in dct['task_specs'].items(): for name, task_dict in dct['task_specs'].items():
# I hate this, but I need to pass in the workflow spec when I create the task. # I hate this, but I need to pass in the workflow spec when I create the task.
# IMO storing the workflow spec on the task spec is a TERRIBLE idea, but that's # IMO storing the workflow spec on the task spec is a TERRIBLE idea, but that's

View File

@ -1,68 +1,75 @@
from .helpers.spec import TaskSpecConverter # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ...specs.StartTask import StartTask from SpiffWorkflow.bpmn.specs.control import BpmnStartTask, _EndJoin, _BoundaryEventParent, SimpleBpmnTask
from ...specs.Simple import Simple from SpiffWorkflow.bpmn.specs.bpmn_task_spec import _BpmnCondition
from SpiffWorkflow.bpmn.specs.defaults import (
from ..specs.BpmnProcessSpec import _EndJoin UserTask,
from ..specs.BpmnSpecMixin import _BpmnCondition ManualTask,
from ..specs.NoneTask import NoneTask NoneTask,
from ..specs.UserTask import UserTask ScriptTask,
from ..specs.ManualTask import ManualTask ExclusiveGateway,
from ..specs.ScriptTask import ScriptTask InclusiveGateway,
from ..specs.MultiInstanceTask import StandardLoopTask, SequentialMultiInstanceTask, ParallelMultiInstanceTask ParallelGateway,
from ..specs.SubWorkflowTask import CallActivity, TransactionSubprocess, SubWorkflowTask StandardLoopTask,
from ..specs.ExclusiveGateway import ExclusiveGateway SequentialMultiInstanceTask,
from ..specs.InclusiveGateway import InclusiveGateway ParallelMultiInstanceTask,
from ..specs.ParallelGateway import ParallelGateway CallActivity,
from ..specs.events.StartEvent import StartEvent TransactionSubprocess,
from ..specs.events.EndEvent import EndEvent SubWorkflowTask,
from ..specs.events.IntermediateEvent import ( StartEvent,
BoundaryEvent, EndEvent,
_BoundaryEventParent,
EventBasedGateway,
IntermediateCatchEvent, IntermediateCatchEvent,
IntermediateThrowEvent, IntermediateThrowEvent,
BoundaryEvent,
EventBasedGateway,
SendTask, SendTask,
ReceiveTask, ReceiveTask,
) )
from .helpers.spec import TaskSpecConverter
class DefaultTaskSpecConverter(TaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
return dct
def from_dict(self, dct):
return self.task_spec_from_dict(dct)
class SimpleTaskConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(Simple, registry)
class StartTaskConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(StartTask, registry)
class EndJoinConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(_EndJoin, registry)
class BpmnTaskSpecConverter(TaskSpecConverter): class BpmnTaskSpecConverter(TaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
return dct return dct
def from_dict(self, dct): def from_dict(self, dct):
return self.task_spec_from_dict(dct) return self.task_spec_from_dict(dct)
class SimpleBpmnTaskConverter(BpmnTaskSpecConverter):
def __init__(self, registry):
super().__init__(SimpleBpmnTask, registry)
class BpmnStartTaskConverter(BpmnTaskSpecConverter):
def __init__(self, registry):
super().__init__(BpmnStartTask, registry)
class EndJoinConverter(BpmnTaskSpecConverter):
def __init__(self, registry):
super().__init__(_EndJoin, registry)
class NoneTaskConverter(BpmnTaskSpecConverter): class NoneTaskConverter(BpmnTaskSpecConverter):
def __init__(self, registry): def __init__(self, registry):
super().__init__(NoneTask, registry) super().__init__(NoneTask, registry)
@ -85,7 +92,6 @@ class ScriptTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['script'] = spec.script dct['script'] = spec.script
return dct return dct
@ -97,7 +103,6 @@ class StandardLoopTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct.update(self.get_standard_loop_attributes(spec)) dct.update(self.get_standard_loop_attributes(spec))
return dct return dct
@ -106,7 +111,6 @@ class MultiInstanceTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['task_spec'] = spec.task_spec dct['task_spec'] = spec.task_spec
dct['cardinality'] = spec.cardinality dct['cardinality'] = spec.cardinality
dct['data_input'] = self.registry.convert(spec.data_input) dct['data_input'] = self.registry.convert(spec.data_input)
@ -294,8 +298,8 @@ class EventBasedGatewayConverter(EventConverter):
DEFAULT_TASK_SPEC_CONVERTER_CLASSES = [ DEFAULT_TASK_SPEC_CONVERTER_CLASSES = [
SimpleTaskConverter, SimpleBpmnTaskConverter,
StartTaskConverter, BpmnStartTaskConverter,
EndJoinConverter, EndJoinConverter,
NoneTaskConverter, NoneTaskConverter,
UserTaskConverter, UserTaskConverter,

View File

@ -1,11 +1,30 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import json import json
import gzip import gzip
from copy import deepcopy from copy import deepcopy
from uuid import UUID from uuid import UUID
from ..workflow import BpmnMessage, BpmnWorkflow from SpiffWorkflow.task import Task
from ..specs.SubWorkflowTask import SubWorkflowTask from SpiffWorkflow.bpmn.workflow import BpmnMessage, BpmnWorkflow
from ...task import Task from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import SubWorkflowTask
from .migration.version_migration import MIGRATIONS from .migration.version_migration import MIGRATIONS
from .helpers.registry import DefaultRegistry from .helpers.registry import DefaultRegistry
@ -134,7 +153,7 @@ class BpmnWorkflowSerializer:
dct = self.__get_dict(serialization, use_gzip) dct = self.__get_dict(serialization, use_gzip)
if self.VERSION_KEY in dct: if self.VERSION_KEY in dct:
return dct[self.VERSION_KEY] return dct[self.VERSION_KEY]
except: # Don't bail out trying to get a version, just return none. except Exception: # Don't bail out trying to get a version, just return none.
return None return None
def workflow_to_dict(self, workflow): def workflow_to_dict(self, workflow):
@ -260,7 +279,7 @@ class BpmnWorkflowSerializer:
for child_task_id in task_dict['children']: for child_task_id in task_dict['children']:
if child_task_id in process_dct['tasks']: if child_task_id in process_dct['tasks']:
child = process_dct['tasks'][child_task_id] process_dct['tasks'][child_task_id]
self.task_tree_from_dict(process_dct, child_task_id, task, process, top, top_dct) self.task_tree_from_dict(process_dct, child_task_id, task, process, top, top_dct)
else: else:
raise ValueError(f"Task {task_id} ({task_spec.name}) has child {child_task_id}, but no such task exists") raise ValueError(f"Task {task_id} ({task_spec.name}) has child {child_task_id}, but no such task exists")

View File

@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ...task import TaskState
from .UnstructuredJoin import UnstructuredJoin
from ...specs.Simple import Simple
from ...specs.WorkflowSpec import WorkflowSpec
class _EndJoin(UnstructuredJoin):
def _check_threshold_unstructured(self, my_task, force=False):
# Look at the tree to find all ready and waiting tasks (excluding
# ourself). The EndJoin waits for everyone!
waiting_tasks = []
for task in my_task.workflow.get_tasks(TaskState.READY | TaskState.WAITING):
if task.thread_id != my_task.thread_id:
continue
if task.task_spec == my_task.task_spec:
continue
is_mine = False
w = task.workflow
if w == my_task.workflow:
is_mine = True
while w and w.outer_workflow != w:
w = w.outer_workflow
if w == my_task.workflow:
is_mine = True
if is_mine:
waiting_tasks.append(task)
return force or len(waiting_tasks) == 0, waiting_tasks
def _run_hook(self, my_task):
result = super(_EndJoin, self)._run_hook(my_task)
my_task.workflow.data.update(my_task.data)
return result
class BpmnProcessSpec(WorkflowSpec):
"""
This class represents the specification of a BPMN process workflow. This
specialises the standard Spiff WorkflowSpec class with a few extra methods
and attributes.
"""
def __init__(self, name=None, description=None, filename=None, svg=None):
"""
Constructor.
:param svg: This provides the SVG representation of the workflow as an
LXML node. (optional)
"""
super(BpmnProcessSpec, self).__init__(name=name, filename=filename)
self.end = _EndJoin(self, '%s.EndJoin' % (self.name))
self.end.connect(Simple(self, 'End'))
self.svg = svg
self.description = description
self.io_specification = None
self.data_objects = {}
self.data_stores = {}
self.correlation_keys = {}

View File

@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ..exceptions import WorkflowDataException
from ...operators import Operator
from ...specs.base import TaskSpec
class _BpmnCondition(Operator):
def __init__(self, *args):
if len(args) > 1:
raise TypeError("Too many arguments")
super(_BpmnCondition, self).__init__(*args)
def _matches(self, task):
return task.workflow.script_engine.evaluate(task, self.args[0])
class BpmnSpecMixin(TaskSpec):
"""
All BPMN spec classes should mix this superclass in. It adds a number of
methods that are BPMN specific to the TaskSpec.
"""
def __init__(self, wf_spec, name, lane=None, position=None, **kwargs):
"""
Constructor.
:param lane: Indicates the name of the lane that this task belongs to
(optional).
"""
super(BpmnSpecMixin, self).__init__(wf_spec, name, **kwargs)
self.lane = lane
self.position = position or {'x': 0, 'y': 0}
self.documentation = None
self.data_input_associations = []
self.data_output_associations = []
self.io_specification = None
@property
def spec_type(self):
return 'BPMN Task'
def connect_outgoing_if(self, condition, taskspec):
"""
Connect this task spec to the indicated child, if the condition
evaluates to true. This should only be called if the task has a
connect_if method (e.g. ExclusiveGateway).
"""
if condition is None:
self.connect(taskspec)
else:
self.connect_if(_BpmnCondition(condition), taskspec)
def _update_hook(self, my_task):
super()._update_hook(my_task)
# This copies data from data objects
for obj in self.data_input_associations:
obj.get(my_task)
# If an IO spec was given, require all inputs are present, and remove all other inputs.
if self.io_specification is not None and len(self.io_specification.data_inputs) > 0:
data = {}
for var in self.io_specification.data_inputs:
if var.name not in my_task.data:
raise WorkflowDataException(f"Missing data input", task=my_task, data_input=var)
data[var.name] = my_task.data[var.name]
my_task.data = data
return True
def _on_complete_hook(self, my_task):
if isinstance(my_task.parent.task_spec, BpmnSpecMixin):
my_task.parent.task_spec._child_complete_hook(my_task)
if self.io_specification is not None and len(self.io_specification.data_outputs) > 0:
data = {}
for var in self.io_specification.data_outputs:
if var.name not in my_task.data:
raise WorkflowDataException(f"Missing data ouput", task=my_task, data_output=var)
data[var.name] = my_task.data[var.name]
my_task.data = data
for obj in self.data_output_associations:
obj.set(my_task)
for obj in self.data_input_associations:
# Remove the any copied input variables that might not have already been removed
my_task.data.pop(obj.name, None)
super(BpmnSpecMixin, self)._on_complete_hook(my_task)
def _child_complete_hook(self, child_task):
pass

View File

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from .ScriptTask import ScriptEngineTask
class ServiceTask(ScriptEngineTask):
"""
Task Spec for a bpmn:serviceTask node.
"""
def __init__(self, wf_spec, name, **kwargs):
super(ServiceTask, self).__init__(wf_spec, name, **kwargs)
@property
def spec_type(self):
return 'Service Task'

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -0,0 +1,50 @@
# Copyright (C) 2012 Matthew Hampton, 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
from SpiffWorkflow.bpmn.specs.control import _EndJoin, BpmnStartTask, SimpleBpmnTask
class BpmnProcessSpec(WorkflowSpec):
"""
This class represents the specification of a BPMN process workflow. This
specialises the standard Spiff WorkflowSpec class with a few extra methods
and attributes.
"""
def __init__(self, name=None, description=None, filename=None, svg=None):
"""
Constructor.
:param svg: This provides the SVG representation of the workflow as an
LXML node. (optional)
"""
super(BpmnProcessSpec, self).__init__(name=name, filename=filename, nostart=True)
# Add a root task to ensure all tasks in the workflow are bpmn tasks
# The serializer ignores this task
SimpleBpmnTask(self, 'Root')
self.start = BpmnStartTask(self, 'Start')
self.end = _EndJoin(self, '%s.EndJoin' % (self.name))
self.end.connect(SimpleBpmnTask(self, 'End'))
self.svg = svg
self.description = description
self.io_specification = None
self.data_objects = {}
self.data_stores = {}
self.correlation_keys = {}

View File

@ -0,0 +1,101 @@
from SpiffWorkflow.bpmn.exceptions import WorkflowDataException
from SpiffWorkflow.operators import Operator
from SpiffWorkflow.specs.base import TaskSpec
class _BpmnCondition(Operator):
def __init__(self, *args):
if len(args) > 1:
raise TypeError("Too many arguments")
super(_BpmnCondition, self).__init__(*args)
def _matches(self, task):
return task.workflow.script_engine.evaluate(task, self.args[0], external_methods=task.workflow.data)
class BpmnTaskSpec(TaskSpec):
"""
This class provides BPMN-specific attributes.
It is intended to be used with all tasks in a BPMN workflow. Spiff internal tasks (such
as Root, EndJoin, etc) inherit directly from this.
Visible tasks inherit from `BpmnSpecMixin`, which will assign the `bpmn_id` and `bpmn_name`.
The intent is to (1) give all tasks in the workflow the same attributes and (2) provide an
easy way of knowing whether a task appearson the diagram.
"""
def __init__(self, wf_spec, name, lane=None, documentation=None,
data_input_associations=None, data_output_associations=None, **kwargs):
"""
:param lane: Indicates the name of the lane that this task belongs to
:param documentation: the contents of the documentation element
:param data_input_associations: a list of data references to be used as inputs to the task
:param data_output_associations: a list of data references to be used as inputs to the task
"""
super().__init__(wf_spec, name, **kwargs)
self.bpmn_id = None
self.bpmn_name = None
self.lane = lane
self.documentation = documentation
self.data_input_associations = data_input_associations or []
self.data_output_associations = data_output_associations or []
self.io_specification = None
if self.description is None:
self.description = 'BPMN Task'
def connect_outgoing_if(self, condition, taskspec):
"""
Connect this task spec to the indicated child, if the condition
evaluates to true. This should only be called if the task has a
connect_if method (e.g. ExclusiveGateway).
"""
if condition is None:
self.connect(taskspec)
else:
self.connect_if(_BpmnCondition(condition), taskspec)
def _update_hook(self, my_task):
super()._update_hook(my_task)
# This copies data from data objects
for obj in self.data_input_associations:
obj.get(my_task)
# If an IO spec was given, require all inputs are present, and remove all other inputs.
if self.io_specification is not None and len(self.io_specification.data_inputs) > 0:
data = {}
for var in self.io_specification.data_inputs:
if var.bpmn_id not in my_task.data:
raise WorkflowDataException("Missing data input", task=my_task, data_input=var)
data[var.bpmn_id] = my_task.data[var.bpmn_id]
my_task.data = data
return True
def _on_complete_hook(self, my_task):
if my_task.parent:
my_task.parent.task_spec._child_complete_hook(my_task)
if self.io_specification is not None and len(self.io_specification.data_outputs) > 0:
data = {}
for var in self.io_specification.data_outputs:
if var.bpmn_id not in my_task.data:
raise WorkflowDataException("Missing data ouput", task=my_task, data_output=var)
data[var.bpmn_id] = my_task.data[var.bpmn_id]
my_task.data = data
for obj in self.data_output_associations:
obj.set(my_task)
for obj in self.data_input_associations:
# Remove the any copied input variables that might not have already been removed
my_task.data.pop(obj.bpmn_id, None)
super()._on_complete_hook(my_task)
def _child_complete_hook(self, child_task):
pass

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2023 Sartography
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,40 +18,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .event_types import ThrowingEvent, CatchingEvent from SpiffWorkflow.task import TaskState
from ..BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.StartTask import StartTask
from ....specs.Simple import Simple from SpiffWorkflow.bpmn.specs.bpmn_task_spec import BpmnTaskSpec
from ....task import TaskState from SpiffWorkflow.bpmn.specs.mixins.unstructured_join import UnstructuredJoin
from SpiffWorkflow.bpmn.specs.mixins.events.intermediate_event import BoundaryEvent
class SendTask(ThrowingEvent):
@property
def spec_type(self):
return 'Send Task'
class ReceiveTask(CatchingEvent): class BpmnStartTask(BpmnTaskSpec, StartTask):
pass
@property class SimpleBpmnTask(BpmnTaskSpec):
def spec_type(self): pass
return 'Receive Task'
class _BoundaryEventParent(BpmnTaskSpec):
class IntermediateCatchEvent(CatchingEvent):
@property
def spec_type(self):
return f'{self.event_definition.event_type} Catching Event'
class IntermediateThrowEvent(ThrowingEvent):
@property
def spec_type(self):
return f'{self.event_definition.event_type} Throwing Event'
class _BoundaryEventParent(Simple, BpmnSpecMixin):
"""This task is inserted before a task with boundary events.""" """This task is inserted before a task with boundary events."""
# I wonder if this would be better modelled as some type of join. # I wonder if this would be better modelled as some type of join.
@ -58,8 +39,7 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin):
# they're attached to be inputs rather than outputs. # they're attached to be inputs rather than outputs.
def __init__(self, wf_spec, name, main_child_task_spec, **kwargs): def __init__(self, wf_spec, name, main_child_task_spec, **kwargs):
super(_BoundaryEventParent, self).__init__(wf_spec, name, **kwargs)
super(_BoundaryEventParent, self).__init__(wf_spec, name)
self.main_child_task_spec = main_child_task_spec self.main_child_task_spec = main_child_task_spec
@property @property
@ -93,39 +73,32 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin):
child._set_state(state) child._set_state(state)
class BoundaryEvent(CatchingEvent): class _EndJoin(UnstructuredJoin, BpmnTaskSpec):
"""Task Spec for a bpmn:boundaryEvent node."""
def __init__(self, wf_spec, name, event_definition, cancel_activity, **kwargs): def _check_threshold_unstructured(self, my_task, force=False):
""" # Look at the tree to find all ready and waiting tasks (excluding
Constructor. # ourself). The EndJoin waits for everyone!
waiting_tasks = []
for task in my_task.workflow.get_tasks(TaskState.READY | TaskState.WAITING):
if task.thread_id != my_task.thread_id:
continue
if task.task_spec == my_task.task_spec:
continue
:param cancel_activity: True if this is a Cancelling boundary event. is_mine = False
""" w = task.workflow
super(BoundaryEvent, self).__init__(wf_spec, name, event_definition, **kwargs) if w == my_task.workflow:
self.cancel_activity = cancel_activity is_mine = True
while w and w.outer_workflow != w:
w = w.outer_workflow
if w == my_task.workflow:
is_mine = True
if is_mine:
waiting_tasks.append(task)
@property return force or len(waiting_tasks) == 0, waiting_tasks
def spec_type(self):
interrupting = 'Interrupting' if self.cancel_activity else 'Non-Interrupting'
return f'{interrupting} {self.event_definition.event_type} Event'
def catches(self, my_task, event_definition, correlations=None): def _run_hook(self, my_task):
# Boundary events should only be caught while waiting result = super(_EndJoin, self)._run_hook(my_task)
return super(BoundaryEvent, self).catches(my_task, event_definition, correlations) and my_task.state == TaskState.WAITING my_task.workflow.data.update(my_task.data)
return result
class EventBasedGateway(CatchingEvent):
@property
def spec_type(self):
return 'Event Based Gateway'
def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_ready_hook(self, my_task):
seen_events = my_task.internal_data.get('seen_events', [])
for child in my_task.children:
if child.task_spec.event_definition not in seen_events:
child.cancel()

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import logging import logging
from copy import deepcopy from copy import deepcopy
@ -8,13 +27,13 @@ data_log = logging.getLogger('spiff.data')
class BpmnDataSpecification: class BpmnDataSpecification:
def __init__(self, name, description=None): def __init__(self, bpmn_id, bpmn_name=None):
""" """
:param name: the variable (the BPMN ID) :param name: the variable (the BPMN ID)
:param description: a human readable name (the BPMN name) :param description: a human readable name (the BPMN name)
""" """
self.name = name self.bpmn_id = bpmn_id
self.description = description or name self.bpmn_name = bpmn_name
# In the future, we can add schemas defining the objects here. # In the future, we can add schemas defining the objects here.
def get(self, my_task, **kwargs): def get(self, my_task, **kwargs):
@ -25,7 +44,7 @@ class BpmnDataSpecification:
class BpmnDataStoreSpecification(BpmnDataSpecification): class BpmnDataStoreSpecification(BpmnDataSpecification):
def __init__(self, name, description, capacity=None, is_unlimited=None): def __init__(self, bpmn_id, bpmn_name, capacity=None, is_unlimited=None):
""" """
:param name: the name of the task data variable and data store key (the BPMN ID) :param name: the name of the task data variable and data store key (the BPMN ID)
:param description: the task description (the BPMN name) :param description: the task description (the BPMN name)
@ -35,7 +54,7 @@ class BpmnDataStoreSpecification(BpmnDataSpecification):
self.capacity = capacity or 0 self.capacity = capacity or 0
self.is_unlimited = is_unlimited or True self.is_unlimited = is_unlimited or True
# In the future, we can add schemas defining the objects here. # In the future, we can add schemas defining the objects here.
super().__init__(name, description) super().__init__(bpmn_id, bpmn_name)
class BpmnIoSpecification: class BpmnIoSpecification:
@ -50,20 +69,20 @@ class DataObject(BpmnDataSpecification):
def get(self, my_task): def get(self, my_task):
"""Copy a value form the workflow data to the task data.""" """Copy a value form the workflow data to the task data."""
if self.name not in my_task.workflow.data: if self.bpmn_id not in my_task.workflow.data:
message = f"The data object could not be read; '{self.name}' does not exist in the process." message = f"The data object could not be read; '{self.bpmn_id}' does not exist in the process."
raise WorkflowDataException(message, my_task, data_input=self) raise WorkflowDataException(message, my_task, data_input=self)
my_task.data[self.name] = deepcopy(my_task.workflow.data[self.name]) my_task.data[self.bpmn_id] = deepcopy(my_task.workflow.data[self.bpmn_id])
data_log.info(f'Read workflow variable {self.name}', extra=my_task.log_info()) data_log.info(f'Read workflow variable {self.bpmn_id}', extra=my_task.log_info())
def set(self, my_task): def set(self, my_task):
"""Copy a value from the task data to the workflow data""" """Copy a value from the task data to the workflow data"""
if self.name not in my_task.data: if self.bpmn_id not in my_task.data:
message = f"A data object could not be set; '{self.name}' not exist in the task." message = f"A data object could not be set; '{self.bpmn_id}' not exist in the task."
raise WorkflowDataException(message, my_task, data_output=self) raise WorkflowDataException(message, my_task, data_output=self)
my_task.workflow.data[self.name] = deepcopy(my_task.data[self.name]) my_task.workflow.data[self.bpmn_id] = deepcopy(my_task.data[self.bpmn_id])
del my_task.data[self.name] del my_task.data[self.bpmn_id]
data_log.info(f'Set workflow variable {self.name}', extra=my_task.log_info()) data_log.info(f'Set workflow variable {self.bpmn_id}', extra=my_task.log_info())
class TaskDataReference(BpmnDataSpecification): class TaskDataReference(BpmnDataSpecification):

View File

@ -0,0 +1,122 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .mixins.bpmn_spec_mixin import BpmnSpecMixin
from .mixins.manual_task import ManualTask as ManualTaskMixin
from .mixins.none_task import NoneTask as NoneTaskMixin
from .mixins.user_task import UserTask as UserTaskMixin
from .mixins.exclusive_gateway import ExclusiveGateway as ExclusiveGatewayMixin
from .mixins.inclusive_gateway import InclusiveGateway as InclusiveGatewayMixin
from .mixins.parallel_gateway import ParallelGateway as ParallelGatewayMixin
from .mixins.script_task import ScriptTask as ScriptTaskMixin
from .mixins.service_task import ServiceTask as ServiceTaskMixin
from .mixins.multiinstance_task import (
StandardLoopTask as StandardLoopTaskMixin,
ParallelMultiInstanceTask as ParallelMultiInstanceTaskMixin,
SequentialMultiInstanceTask as SequentialMultiInstanceTaskMixin,
)
from .mixins.subworkflow_task import (
SubWorkflowTask as SubworkflowTaskMixin,
CallActivity as CallActivityMixin,
TransactionSubprocess as TransactionSubprocessMixin,
)
from .mixins.events.start_event import StartEvent as StartEventMixin
from .mixins.events.end_event import EndEvent as EndEventMixin
from .mixins.events.intermediate_event import (
IntermediateCatchEvent as IntermediateCatchEventMixin,
IntermediateThrowEvent as IntermediateThrowEventMixin,
SendTask as SendTaskMixin,
ReceiveTask as ReceiveTaskMixin,
EventBasedGateway as EventBasedGatewayMixin,
BoundaryEvent as BoundaryEventMixin,
)
# In the future, we could have the parser take a bpmn task spec and construct these classes automatically
# However, I am NOT going to try to do that with the parser we have now
class ManualTask(ManualTaskMixin, BpmnSpecMixin):
pass
class NoneTask(NoneTaskMixin, BpmnSpecMixin):
pass
class UserTask(UserTaskMixin, BpmnSpecMixin):
pass
class ExclusiveGateway(ExclusiveGatewayMixin, BpmnSpecMixin):
pass
class InclusiveGateway(InclusiveGatewayMixin, BpmnSpecMixin):
pass
class ParallelGateway(ParallelGatewayMixin, BpmnSpecMixin):
pass
class ScriptTask(ScriptTaskMixin, BpmnSpecMixin):
pass
class ServiceTask(ServiceTaskMixin, BpmnSpecMixin):
pass
class StandardLoopTask(StandardLoopTaskMixin, BpmnSpecMixin):
pass
class ParallelMultiInstanceTask(ParallelMultiInstanceTaskMixin, BpmnSpecMixin):
pass
class SequentialMultiInstanceTask(SequentialMultiInstanceTaskMixin, BpmnSpecMixin):
pass
class SubWorkflowTask(SubworkflowTaskMixin, BpmnSpecMixin):
pass
class CallActivity(CallActivityMixin, BpmnSpecMixin):
pass
class TransactionSubprocess(TransactionSubprocessMixin, BpmnSpecMixin):
pass
class StartEvent(StartEventMixin, BpmnSpecMixin):
pass
class EndEvent(EndEventMixin, BpmnSpecMixin):
pass
class IntermediateCatchEvent(IntermediateCatchEventMixin, BpmnSpecMixin):
pass
class IntermediateThrowEvent(IntermediateThrowEventMixin, BpmnSpecMixin):
pass
class SendTask(SendTaskMixin, BpmnSpecMixin):
pass
class ReceiveTask(ReceiveTaskMixin, BpmnSpecMixin):
pass
class EventBasedGateway(EventBasedGatewayMixin, BpmnSpecMixin):
pass
class BoundaryEvent(BoundaryEventMixin, BpmnSpecMixin):
pass

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -24,7 +24,6 @@ from time import timezone as tzoffset, altzone as dstoffset, daylight as isdst
from copy import deepcopy from copy import deepcopy
from SpiffWorkflow.exceptions import WorkflowException from SpiffWorkflow.exceptions import WorkflowException
from SpiffWorkflow.task import TaskState
seconds_from_utc = dstoffset if isdst else tzoffset seconds_from_utc = dstoffset if isdst else tzoffset
LOCALTZ = timezone(timedelta(seconds=-1 * seconds_from_utc)) LOCALTZ = timezone(timedelta(seconds=-1 * seconds_from_utc))
@ -42,11 +41,12 @@ class EventDefinition(object):
and external flags. and external flags.
Default catch behavior is to set the event to fired Default catch behavior is to set the event to fired
""" """
def __init__(self): def __init__(self, description=None):
# Ideally I'd mke these parameters, but I don't want to them to be parameters # Ideally I'd mke these parameters, but I don't want to them to be parameters
# for any subclasses (as they are based on event type, not user choice) and # for any subclasses (as they are based on event type, not user choice) and
# I don't want to write a separate deserializer for every every type. # I don't want to write a separate deserializer for every every type.
self.internal, self.external = True, True self.internal, self.external = True, True
self.description = description
@property @property
def event_type(self): def event_type(self):
@ -92,8 +92,8 @@ class NamedEventDefinition(EventDefinition):
:param name: the name of this event :param name: the name of this event
""" """
def __init__(self, name): def __init__(self, name, **kwargs):
super(NamedEventDefinition, self).__init__() super(NamedEventDefinition, self).__init__(**kwargs)
self.name = name self.name = name
def reset(self, my_task): def reset(self, my_task):
@ -108,14 +108,10 @@ class CancelEventDefinition(EventDefinition):
Cancel events are only handled by the outerworkflow, as they can only be used inside Cancel events are only handled by the outerworkflow, as they can only be used inside
of transaction subprocesses. of transaction subprocesses.
""" """
def __init__(self): def __init__(self, **kwargs):
super(CancelEventDefinition, self).__init__() super(CancelEventDefinition, self).__init__(**kwargs)
self.internal = False self.internal = False
@property
def event_type(self):
return 'Cancel'
class ErrorEventDefinition(NamedEventDefinition): class ErrorEventDefinition(NamedEventDefinition):
""" """
@ -123,15 +119,11 @@ class ErrorEventDefinition(NamedEventDefinition):
matched by code rather than name. matched by code rather than name.
""" """
def __init__(self, name, error_code=None): def __init__(self, name, error_code=None, **kwargs):
super(ErrorEventDefinition, self).__init__(name) super(ErrorEventDefinition, self).__init__(name,**kwargs)
self.error_code = error_code self.error_code = error_code
self.internal = False self.internal = False
@property
def event_type(self):
return 'Error'
def __eq__(self, other): def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.error_code in [ None, other.error_code ] return self.__class__.__name__ == other.__class__.__name__ and self.error_code in [ None, other.error_code ]
@ -142,20 +134,16 @@ class EscalationEventDefinition(NamedEventDefinition):
the spec says that the escalation code should be matched. the spec says that the escalation code should be matched.
""" """
def __init__(self, name, escalation_code=None): def __init__(self, name, escalation_code=None, **kwargs):
""" """
Constructor. Constructor.
:param escalation_code: The escalation code this event should :param escalation_code: The escalation code this event should
react to. If None then all escalations will activate this event. react to. If None then all escalations will activate this event.
""" """
super(EscalationEventDefinition, self).__init__(name) super(EscalationEventDefinition, self).__init__(name, **kwargs)
self.escalation_code = escalation_code self.escalation_code = escalation_code
@property
def event_type(self):
return 'Escalation'
def __eq__(self, other): def __eq__(self, other):
return self.__class__.__name__ == other.__class__.__name__ and self.escalation_code in [ None, other.escalation_code ] return self.__class__.__name__ == other.__class__.__name__ and self.escalation_code in [ None, other.escalation_code ]
@ -171,17 +159,13 @@ class CorrelationProperty:
class MessageEventDefinition(NamedEventDefinition): class MessageEventDefinition(NamedEventDefinition):
"""The default message event.""" """The default message event."""
def __init__(self, name, correlation_properties=None): def __init__(self, name, correlation_properties=None, **kwargs):
super().__init__(name) super().__init__(name, **kwargs)
self.correlation_properties = correlation_properties or [] self.correlation_properties = correlation_properties or []
self.payload = None self.payload = None
self.internal = False self.internal = False
@property def catch(self, my_task, event_definition=None):
def event_type(self):
return 'Message'
def catch(self, my_task, event_definition = None):
self.update_internal_data(my_task, event_definition) self.update_internal_data(my_task, event_definition)
super(MessageEventDefinition, self).catch(my_task, event_definition) super(MessageEventDefinition, self).catch(my_task, event_definition)
@ -243,13 +227,10 @@ class NoneEventDefinition(EventDefinition):
""" """
This class defines behavior for NoneEvents. We override throw to do nothing. This class defines behavior for NoneEvents. We override throw to do nothing.
""" """
def __init__(self): def __init__(self, **kwargs):
super().__init__(**kwargs)
self.internal, self.external = False, False self.internal, self.external = False, False
@property
def event_type(self):
return 'Default'
def throw(self, my_task): def throw(self, my_task):
"""It's a 'none' event, so nothing to throw.""" """It's a 'none' event, so nothing to throw."""
pass pass
@ -261,26 +242,21 @@ class NoneEventDefinition(EventDefinition):
class SignalEventDefinition(NamedEventDefinition): class SignalEventDefinition(NamedEventDefinition):
"""The SignalEventDefinition is the implementation of event definition used for Signal Events.""" """The SignalEventDefinition is the implementation of event definition used for Signal Events."""
def __init__(self, name, **kwargs):
super().__init__(name, **kwargs)
@property
def spec_type(self):
return 'Signal'
class TerminateEventDefinition(EventDefinition): class TerminateEventDefinition(EventDefinition):
"""The TerminateEventDefinition is the implementation of event definition used for Termination Events.""" """The TerminateEventDefinition is the implementation of event definition used for Termination Events."""
def __init__(self): def __init__(self, **kwargs):
super(TerminateEventDefinition, self).__init__() super(TerminateEventDefinition, self).__init__(**kwargs)
self.external = False self.external = False
@property
def event_type(self):
return 'Terminate'
class TimerEventDefinition(EventDefinition): class TimerEventDefinition(EventDefinition):
def __init__(self, name, expression): def __init__(self, name, expression, **kwargs):
""" """
Constructor. Constructor.
@ -288,7 +264,7 @@ class TimerEventDefinition(EventDefinition):
:param expression: An ISO 8601 datetime or interval expression. :param expression: An ISO 8601 datetime or interval expression.
""" """
super().__init__() super().__init__(**kwargs)
self.name = name self.name = name
self.expression = expression self.expression = expression
@ -362,7 +338,7 @@ class TimerEventDefinition(EventDefinition):
@staticmethod @staticmethod
def parse_iso_week(expression): def parse_iso_week(expression):
# https://en.wikipedia.org/wiki/ISO_8601#Week_dates # https://en.wikipedia.org/wiki/ISO_8601#Week_dates
m = re.match('(\d{4})W(\d{2})(\d)(T.+)?', expression.upper().replace('-', '')) m = re.match(r'(\d{4})W(\d{2})(\d)(T.+)?', expression.upper().replace('-', ''))
year, month, day, ts = m.groups() year, month, day, ts = m.groups()
ds = datetime.fromisocalendar(int(year), int(month), int(day)).strftime('%Y-%m-%d') ds = datetime.fromisocalendar(int(year), int(month), int(day)).strftime('%Y-%m-%d')
return TimerEventDefinition.get_datetime(ds + (ts or '')) return TimerEventDefinition.get_datetime(ds + (ts or ''))
@ -409,10 +385,6 @@ class TimerEventDefinition(EventDefinition):
class TimeDateEventDefinition(TimerEventDefinition): class TimeDateEventDefinition(TimerEventDefinition):
"""A Timer event represented by a specific date/time.""" """A Timer event represented by a specific date/time."""
@property
def event_type(self):
return 'Time Date Timer'
def has_fired(self, my_task): def has_fired(self, my_task):
event_value = my_task._get_internal_data('event_value') event_value = my_task._get_internal_data('event_value')
if event_value is None: if event_value is None:
@ -429,10 +401,6 @@ class TimeDateEventDefinition(TimerEventDefinition):
class DurationTimerEventDefinition(TimerEventDefinition): class DurationTimerEventDefinition(TimerEventDefinition):
"""A timer event represented by a duration""" """A timer event represented by a duration"""
@property
def event_type(self):
return 'Duration Timer'
def has_fired(self, my_task): def has_fired(self, my_task):
event_value = my_task._get_internal_data("event_value") event_value = my_task._get_internal_data("event_value")
if event_value is None: if event_value is None:
@ -450,10 +418,6 @@ class DurationTimerEventDefinition(TimerEventDefinition):
class CycleTimerEventDefinition(TimerEventDefinition): class CycleTimerEventDefinition(TimerEventDefinition):
@property
def event_type(self):
return 'Cycle Timer'
def cycle_complete(self, my_task): def cycle_complete(self, my_task):
event_value = my_task._get_internal_data('event_value') event_value = my_task._get_internal_data('event_value')
@ -489,15 +453,11 @@ class CycleTimerEventDefinition(TimerEventDefinition):
class MultipleEventDefinition(EventDefinition): class MultipleEventDefinition(EventDefinition):
def __init__(self, event_definitions=None, parallel=False): def __init__(self, event_definitions=None, parallel=False, **kwargs):
super().__init__() super().__init__(**kwargs)
self.event_definitions = event_definitions or [] self.event_definitions = event_definitions or []
self.parallel = parallel self.parallel = parallel
@property
def event_type(self):
return 'Multiple'
def has_fired(self, my_task): def has_fired(self, my_task):
seen_events = my_task.internal_data.get('seen_events', []) seen_events = my_task.internal_data.get('seen_events', [])

View File

@ -0,0 +1,28 @@
# Copyright (C) 2012 Matthew Hampton, 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ..bpmn_task_spec import BpmnTaskSpec
class BpmnSpecMixin(BpmnTaskSpec):
def __init__(self, wf_spec, bpmn_id, **kwargs):
super().__init__(wf_spec, bpmn_id, **kwargs)
self.bpmn_id = bpmn_id
self.bpmn_name = kwargs.get('bpmn_name')

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,9 +17,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from SpiffWorkflow.task import TaskState
from .event_types import ThrowingEvent from .event_types import ThrowingEvent
from .event_definitions import TerminateEventDefinition, CancelEventDefinition from ...event_definitions import TerminateEventDefinition, CancelEventDefinition
from ....task import TaskState
class EndEvent(ThrowingEvent): class EndEvent(ThrowingEvent):
@ -41,14 +41,6 @@ class EndEvent(ThrowingEvent):
Gateways, one of the associated Events has been triggered. Gateways, one of the associated Events has been triggered.
* There is no token remaining within the Process instance. * There is no token remaining within the Process instance.
""" """
def __init__(self, wf_spec, name, event_definition, **kwargs):
super(EndEvent, self).__init__(wf_spec, name, event_definition, **kwargs)
@property
def spec_type(self):
return 'End Event'
def _on_complete_hook(self, my_task): def _on_complete_hook(self, my_task):
super(EndEvent, self)._on_complete_hook(my_task) super(EndEvent, self)._on_complete_hook(my_task)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,22 +16,22 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import time
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.specs.base import TaskSpec
from ...event_definitions import MessageEventDefinition, NoneEventDefinition, CycleTimerEventDefinition
from .event_definitions import MessageEventDefinition, NoneEventDefinition, CycleTimerEventDefinition
from ..BpmnSpecMixin import BpmnSpecMixin
from ....specs.Simple import Simple
from ....task import TaskState
class CatchingEvent(Simple, BpmnSpecMixin): class CatchingEvent(TaskSpec):
"""Base Task Spec for Catching Event nodes.""" """Base Task Spec for Catching Event nodes."""
def __init__(self, wf_spec, name, event_definition, **kwargs): def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs):
""" """
Constructor. Constructor.
:param event_definition: the EventDefinition that we must wait for. :param event_definition: the EventDefinition that we must wait for.
""" """
super(CatchingEvent, self).__init__(wf_spec, name, **kwargs) super(CatchingEvent, self).__init__(wf_spec, bpmn_id, **kwargs)
self.event_definition = event_definition self.event_definition = event_definition
def catches(self, my_task, event_definition, correlations=None): def catches(self, my_task, event_definition, correlations=None):
@ -46,6 +46,7 @@ class CatchingEvent(Simple, BpmnSpecMixin):
definition, at which point we can update our task's state. definition, at which point we can update our task's state.
""" """
self.event_definition.catch(my_task, event_definition) self.event_definition.catch(my_task, event_definition)
my_task.last_update_time = time.time()
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
def _update_hook(self, my_task): def _update_hook(self, my_task):
@ -74,26 +75,17 @@ class CatchingEvent(Simple, BpmnSpecMixin):
self.event_definition.reset(my_task) self.event_definition.reset(my_task)
return super(CatchingEvent, self)._run_hook(my_task) return super(CatchingEvent, self)._run_hook(my_task)
# This fixes the problem of boundary events remaining cancelled if the task is reused.
# It pains me to add these methods, but unless we can get rid of the loop reset task we're stuck
def task_should_set_children_future(self, my_task): class ThrowingEvent(TaskSpec):
return True
def task_will_set_children_future(self, my_task):
my_task.internal_data = {}
class ThrowingEvent(Simple, BpmnSpecMixin):
"""Base Task Spec for Throwing Event nodes.""" """Base Task Spec for Throwing Event nodes."""
def __init__(self, wf_spec, name, event_definition, **kwargs): def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs):
""" """
Constructor. Constructor.
:param event_definition: the EventDefinition to be thrown. :param event_definition: the EventDefinition to be thrown.
""" """
super(ThrowingEvent, self).__init__(wf_spec, name, **kwargs) super(ThrowingEvent, self).__init__(wf_spec, bpmn_id, **kwargs)
self.event_definition = event_definition self.event_definition = event_definition
def _run_hook(self, my_task): def _run_hook(self, my_task):

View File

@ -0,0 +1,63 @@
# Copyright (C) 2012 Matthew Hampton, 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.task import TaskState
from .event_types import ThrowingEvent, CatchingEvent
class SendTask(ThrowingEvent):
pass
class ReceiveTask(CatchingEvent):
pass
class IntermediateCatchEvent(CatchingEvent):
pass
class IntermediateThrowEvent(ThrowingEvent):
pass
class BoundaryEvent(CatchingEvent):
"""Task Spec for a bpmn:boundaryEvent node."""
def __init__(self, wf_spec, bpmn_id, event_definition, cancel_activity, **kwargs):
"""
Constructor.
:param cancel_activity: True if this is a Cancelling boundary event.
"""
super(BoundaryEvent, self).__init__(wf_spec, bpmn_id, event_definition, **kwargs)
self.cancel_activity = cancel_activity
def catches(self, my_task, event_definition, correlations=None):
# Boundary events should only be caught while waiting
return super(BoundaryEvent, self).catches(my_task, event_definition, correlations) and my_task.state == TaskState.WAITING
class EventBasedGateway(CatchingEvent):
def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_ready_hook(self, my_task):
seen_events = my_task.internal_data.get('seen_events', [])
for child in my_task.children:
if child.task_spec.event_definition not in seen_events:
child.cancel()

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,27 +17,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from SpiffWorkflow.task import TaskState
from .event_types import CatchingEvent from .event_types import CatchingEvent
from ....task import TaskState
class StartEvent(CatchingEvent): class StartEvent(CatchingEvent):
"""Task Spec for a bpmn:startEvent node with an optional event definition.""" """Task Spec for a bpmn:startEvent node with an optional event definition."""
def __init__(self, wf_spec, name, event_definition, **kwargs):
super(StartEvent, self).__init__(wf_spec, name, event_definition, **kwargs)
@property
def spec_type(self):
return f'{self.event_definition.event_type} Start Event'
def catch(self, my_task, event_definition): def catch(self, my_task, event_definition):
# We might need to revisit a start event after it completes or # We might need to revisit a start event after it completes or
# if it got cancelled so we'll still catch messages even if we're finished # if it got cancelled so we'll still catch messages even if we're finished
if my_task.state == TaskState.COMPLETED or my_task.state == TaskState.CANCELLED: if my_task.state == TaskState.COMPLETED or my_task.state == TaskState.CANCELLED:
my_task.set_children_future() my_task.workflow.reset_from_task_id(my_task.id)
my_task._set_state(TaskState.WAITING)
super(StartEvent, self).catch(my_task, event_definition) super(StartEvent, self).catch(my_task, event_definition)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,12 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.ExclusiveChoice import ExclusiveChoice
from ...specs.ExclusiveChoice import ExclusiveChoice from SpiffWorkflow.specs.MultiChoice import MultiChoice
from ...specs.MultiChoice import MultiChoice
class ExclusiveGateway(ExclusiveChoice, BpmnSpecMixin): class ExclusiveGateway(ExclusiveChoice):
""" """
Task Spec for a bpmn:exclusiveGateway node. Task Spec for a bpmn:exclusiveGateway node.
""" """
@ -31,6 +30,3 @@ class ExclusiveGateway(ExclusiveChoice, BpmnSpecMixin):
# Bypass the check for no default output -- this is not required in BPMN # Bypass the check for no default output -- this is not required in BPMN
MultiChoice.test(self) MultiChoice.test(self)
@property
def spec_type(self):
return 'Exclusive Gateway'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,10 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from SpiffWorkflow.exceptions import WorkflowTaskException from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from ...task import TaskState from SpiffWorkflow.task import TaskState
from .UnstructuredJoin import UnstructuredJoin from SpiffWorkflow.specs.MultiChoice import MultiChoice
from ...specs.MultiChoice import MultiChoice from .unstructured_join import UnstructuredJoin
class InclusiveGateway(MultiChoice, UnstructuredJoin): class InclusiveGateway(MultiChoice, UnstructuredJoin):
@ -113,10 +114,6 @@ class InclusiveGateway(MultiChoice, UnstructuredJoin):
def _run_hook(self, my_task): def _run_hook(self, my_task):
outputs = self._get_matching_outputs(my_task) outputs = self._get_matching_outputs(my_task)
if len(outputs) == 0: if len(outputs) == 0:
raise WorkflowTaskException(f'No conditions satisfied on gateway', task=my_task) raise WorkflowTaskException('No conditions satisfied on gateway', task=my_task)
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
return True return True
@property
def spec_type(self):
return 'Inclusive Gateway'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton # Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,16 +16,13 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from ...bpmn.specs.BpmnSpecMixin import BpmnSpecMixin
from ...specs.Simple import Simple from SpiffWorkflow.specs.base import TaskSpec
class ManualTask(Simple, BpmnSpecMixin): class ManualTask(TaskSpec):
"""Task Spec for a bpmn:manualTask node."""
def is_engine_task(self): def __init__(self, wf_spec, bpmn_id, **kwargs):
return False super().__init__(wf_spec, bpmn_id, **kwargs)
self.manual = True
@property
def spec_type(self):
return 'Manual Task'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2023 Sartography
# Copyright (C) 2020 Sartography
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -20,17 +20,17 @@
from copy import deepcopy from copy import deepcopy
from collections.abc import Iterable, Sequence, Mapping, MutableSequence, MutableMapping from collections.abc import Iterable, Sequence, Mapping, MutableSequence, MutableMapping
from ...task import TaskState from SpiffWorkflow.task import TaskState
from ...util.deep_merge import DeepMerge from SpiffWorkflow.specs.base import TaskSpec
from ..exceptions import WorkflowDataException from SpiffWorkflow.util.deep_merge import DeepMerge
from .BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.bpmn.exceptions import WorkflowDataException
class LoopTask(BpmnSpecMixin): class LoopTask(TaskSpec):
def process_children(self, my_task): def process_children(self, my_task):
""" """
Handle any newly completed children and update merged tasks. Handle any newly completed children and update merged tasks.
Returns a boolean indicating whether there is a child currently running Returns a boolean indicating whether there is a child currently running
""" """
merged = my_task.internal_data.get('merged') or [] merged = my_task.internal_data.get('merged') or []
@ -42,7 +42,7 @@ class LoopTask(BpmnSpecMixin):
elif not child._has_state(TaskState.FINISHED_MASK): elif not child._has_state(TaskState.FINISHED_MASK):
child_running = True child_running = True
my_task.internal_data['merged'] = merged my_task.internal_data['merged'] = merged
return child_running return child_running
def child_completed_action(self, my_task, child): def child_completed_action(self, my_task, child):
raise NotImplementedError raise NotImplementedError
@ -50,8 +50,8 @@ class LoopTask(BpmnSpecMixin):
class StandardLoopTask(LoopTask): class StandardLoopTask(LoopTask):
def __init__(self, wf_spec, name, task_spec, maximum, condition, test_before, **kwargs): def __init__(self, wf_spec, bpmn_id, task_spec, maximum, condition, test_before, **kwargs):
super().__init__(wf_spec, name, **kwargs) super().__init__(wf_spec, bpmn_id, **kwargs)
self.task_spec = task_spec self.task_spec = task_spec
self.maximum = maximum self.maximum = maximum
self.condition = condition self.condition = condition
@ -59,7 +59,9 @@ class StandardLoopTask(LoopTask):
def _update_hook(self, my_task): def _update_hook(self, my_task):
super()._update_hook(my_task) if my_task.state != TaskState.WAITING:
super()._update_hook(my_task)
child_running = self.process_children(my_task) child_running = self.process_children(my_task)
if child_running: if child_running:
# We're in the middle of an iteration; we're not done and we can't create a new task # We're in the middle of an iteration; we're not done and we can't create a new task
@ -72,7 +74,7 @@ class StandardLoopTask(LoopTask):
if my_task.state != TaskState.WAITING: if my_task.state != TaskState.WAITING:
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
task_spec = my_task.workflow.spec.task_specs[self.task_spec] task_spec = my_task.workflow.spec.task_specs[self.task_spec]
child = my_task._add_child(task_spec, TaskState.READY) child = my_task._add_child(task_spec, TaskState.WAITING)
child.data = deepcopy(my_task.data) child.data = deepcopy(my_task.data)
def child_completed_action(self, my_task, child): def child_completed_action(self, my_task, child):
@ -92,11 +94,11 @@ class StandardLoopTask(LoopTask):
class MultiInstanceTask(LoopTask): class MultiInstanceTask(LoopTask):
def __init__(self, wf_spec, name, task_spec, cardinality=None, data_input=None, def __init__(self, wf_spec, bpmn_id, task_spec, cardinality=None, data_input=None,
data_output=None, input_item=None, output_item=None, condition=None, data_output=None, input_item=None, output_item=None, condition=None,
**kwargs): **kwargs):
super().__init__(wf_spec, name, **kwargs) super().__init__(wf_spec, bpmn_id, **kwargs)
self.task_spec = task_spec self.task_spec = task_spec
self.cardinality = cardinality self.cardinality = cardinality
self.data_input = data_input self.data_input = data_input
@ -109,13 +111,13 @@ class MultiInstanceTask(LoopTask):
"""This merges child data into this task's data.""" """This merges child data into this task's data."""
if self.data_output is not None and self.output_item is not None: if self.data_output is not None and self.output_item is not None:
if self.output_item.name not in child.data: if self.output_item.bpmn_id not in child.data:
self.raise_data_exception("Expected an output item", child) self.raise_data_exception("Expected an output item", child)
item = child.data[self.output_item.name] item = child.data[self.output_item.bpmn_id]
key_or_index = child.internal_data.get('key_or_index') key_or_index = child.internal_data.get('key_or_index')
data_output = my_task.data[self.data_output.name] data_output = my_task.data[self.data_output.bpmn_id]
data_input = my_task.data[self.data_input.name] if self.data_input is not None else None data_input = my_task.data[self.data_input.bpmn_id] if self.data_input is not None else None
if isinstance(data_output, Mapping) or data_input is data_output: if key_or_index is not None and (isinstance(data_output, Mapping) or data_input is data_output):
data_output[key_or_index] = item data_output[key_or_index] = item
else: else:
data_output.append(item) data_output.append(item)
@ -128,7 +130,7 @@ class MultiInstanceTask(LoopTask):
child = my_task._add_child(task_spec, TaskState.WAITING) child = my_task._add_child(task_spec, TaskState.WAITING)
child.data = deepcopy(my_task.data) child.data = deepcopy(my_task.data)
if self.input_item is not None: if self.input_item is not None:
child.data[self.input_item.name] = deepcopy(item) child.data[self.input_item.bpmn_id] = deepcopy(item)
if key_or_index is not None: if key_or_index is not None:
child.internal_data['key_or_index'] = key_or_index child.internal_data['key_or_index'] = key_or_index
child.task_spec._update(child) child.task_spec._update(child)
@ -142,7 +144,7 @@ class MultiInstanceTask(LoopTask):
def init_data_output_with_input_data(self, my_task, input_data): def init_data_output_with_input_data(self, my_task, input_data):
name = self.data_output.name name = self.data_output.bpmn_id
if name not in my_task.data: if name not in my_task.data:
if isinstance(input_data, (MutableMapping, MutableSequence)): if isinstance(input_data, (MutableMapping, MutableSequence)):
# We can use the same class if it implements __setitem__ # We can use the same class if it implements __setitem__
@ -154,7 +156,7 @@ class MultiInstanceTask(LoopTask):
# For all other types, we'll append to a list # For all other types, we'll append to a list
my_task.data[name] = list() my_task.data[name] = list()
else: else:
output_data = my_task.data[self.data_output.name] output_data = my_task.data[self.data_output.bpmn_id]
if not isinstance(output_data, (MutableSequence, MutableMapping)): if not isinstance(output_data, (MutableSequence, MutableMapping)):
self.raise_data_exception("Only a mutable map (dict) or sequence (list) can be used for output", my_task) self.raise_data_exception("Only a mutable map (dict) or sequence (list) can be used for output", my_task)
if input_data is not output_data and not isinstance(output_data, Mapping) and len(output_data) > 0: if input_data is not output_data and not isinstance(output_data, Mapping) and len(output_data) > 0:
@ -163,7 +165,7 @@ class MultiInstanceTask(LoopTask):
def init_data_output_with_cardinality(self, my_task): def init_data_output_with_cardinality(self, my_task):
name = self.data_output.name name = self.data_output.bpmn_id
if name not in my_task.data: if name not in my_task.data:
my_task.data[name] = list() my_task.data[name] = list()
elif not isinstance(my_task.data[name], MutableMapping) and len(my_task.data[name]) > 0: elif not isinstance(my_task.data[name], MutableMapping) and len(my_task.data[name]) > 0:
@ -207,7 +209,7 @@ class SequentialMultiInstanceTask(MultiInstanceTask):
def get_next_input_item(self, my_task): def get_next_input_item(self, my_task):
input_data = my_task.data[self.data_input.name] input_data = my_task.data[self.data_input.bpmn_id]
remaining = my_task.internal_data.get('remaining') remaining = my_task.internal_data.get('remaining')
if remaining is None: if remaining is None:
@ -229,9 +231,9 @@ class SequentialMultiInstanceTask(MultiInstanceTask):
def init_remaining_items(self, my_task): def init_remaining_items(self, my_task):
if self.data_input.name not in my_task.data: if self.data_input.bpmn_id not in my_task.data:
self.raise_data_exception("Missing data input for multiinstance task", my_task) self.raise_data_exception("Missing data input for multiinstance task", my_task)
input_data = my_task.data[self.data_input.name] input_data = my_task.data[self.data_input.bpmn_id]
# This is internal bookkeeping, so we know where we are; we get the actual items when we create the task # This is internal bookkeeping, so we know where we are; we get the actual items when we create the task
if isinstance(input_data, Sequence): if isinstance(input_data, Sequence):
@ -270,7 +272,7 @@ class SequentialMultiInstanceTask(MultiInstanceTask):
class ParallelMultiInstanceTask(MultiInstanceTask): class ParallelMultiInstanceTask(MultiInstanceTask):
def _update_hook(self, my_task): def _update_hook(self, my_task):
if my_task.state != TaskState.WAITING: if my_task.state != TaskState.WAITING:
@ -287,7 +289,7 @@ class ParallelMultiInstanceTask(MultiInstanceTask):
def create_children(self, my_task): def create_children(self, my_task):
data_input = my_task.data[self.data_input.name] if self.data_input is not None else None data_input = my_task.data[self.data_input.bpmn_id] if self.data_input is not None else None
if data_input is not None: if data_input is not None:
# We have to preserve the key or index for maps/sequences, in case we're updating in place, or the output is a mapping # We have to preserve the key or index for maps/sequences, in case we're updating in place, or the output is a mapping
if isinstance(data_input, Mapping): if isinstance(data_input, Mapping):
@ -306,7 +308,7 @@ class ParallelMultiInstanceTask(MultiInstanceTask):
if self.data_output is not None: if self.data_output is not None:
if self.data_input is not None: if self.data_input is not None:
self.init_data_output_with_input_data(my_task, my_task.data[self.data_input.name]) self.init_data_output_with_input_data(my_task, my_task.data[self.data_input.bpmn_id])
else: else:
self.init_data_output_with_cardinality(my_task) self.init_data_output_with_cardinality(my_task)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,16 +16,13 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from ...specs.Simple import Simple
from ...bpmn.specs.BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.base import TaskSpec
class NoneTask(Simple, BpmnSpecMixin): class NoneTask(TaskSpec):
"""Task Spec for a bpmn:task node."""
def is_engine_task(self): def __init__(self, wf_spec, bpmn_id, **kwargs):
return False super().__init__(wf_spec, bpmn_id, **kwargs)
self.manual = True
@property
def spec_type(self):
return 'Task'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,7 +16,8 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .UnstructuredJoin import UnstructuredJoin
from .unstructured_join import UnstructuredJoin
class ParallelGateway(UnstructuredJoin): class ParallelGateway(UnstructuredJoin):
@ -43,7 +44,3 @@ class ParallelGateway(UnstructuredJoin):
def _check_threshold_unstructured(self, my_task, force=False): def _check_threshold_unstructured(self, my_task, force=False):
completed_inputs, waiting_tasks = self._get_inputs_with_tokens(my_task) completed_inputs, waiting_tasks = self._get_inputs_with_tokens(my_task)
return force or len(completed_inputs) >= len(self.inputs), waiting_tasks return force or len(completed_inputs) >= len(self.inputs), waiting_tasks
@property
def spec_type(self):
return 'Parallel Gateway'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,11 +17,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.base import TaskSpec
from ...specs.Simple import Simple
class ScriptEngineTask(Simple, BpmnSpecMixin): class ScriptEngineTask(TaskSpec):
"""Task Spec for a bpmn:scriptTask node""" """Task Spec for a bpmn:scriptTask node"""
def _execute(self, task): def _execute(self, task):
@ -34,18 +33,14 @@ class ScriptEngineTask(Simple, BpmnSpecMixin):
class ScriptTask(ScriptEngineTask): class ScriptTask(ScriptEngineTask):
def __init__(self, wf_spec, name, script, **kwargs): def __init__(self, wf_spec, bpmn_id, script, **kwargs):
""" """
Constructor. Constructor.
:param script: the script that must be executed by the script engine. :param script: the script that must be executed by the script engine.
""" """
super(ScriptTask, self).__init__(wf_spec, name, **kwargs) super(ScriptTask, self).__init__(wf_spec, bpmn_id, **kwargs)
self.script = script self.script = script
@property
def spec_type(self):
return 'Script Task'
def _execute(self, task): def _execute(self, task):
return task.workflow.script_engine.execute(task, self.script) return task.workflow.script_engine.execute(task, self.script)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,19 +17,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin from .script_task import ScriptEngineTask
from ...specs.Simple import Simple
class UserTask(Simple, BpmnSpecMixin): class ServiceTask(ScriptEngineTask):
""" """
Task Spec for a bpmn:userTask node. Task Spec for a bpmn:serviceTask node.
""" """
def is_engine_task(self): def __init__(self, wf_spec, bpmn_id, **kwargs):
return False super(ServiceTask, self).__init__(wf_spec, bpmn_id, **kwargs)
@property
def spec_type(self):
return 'User Task'

View File

@ -1,30 +1,47 @@
# -*- coding: utf-8 -*- # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from copy import deepcopy from copy import deepcopy
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from .BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.base import TaskSpec
from ..exceptions import WorkflowDataException from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
from SpiffWorkflow.bpmn.specs.mixins.events.intermediate_event import BoundaryEvent
from SpiffWorkflow.bpmn.exceptions import WorkflowDataException
class SubWorkflowTask(BpmnSpecMixin): class SubWorkflowTask(TaskSpec):
""" """
Task Spec for a bpmn node containing a subworkflow. Task Spec for a bpmn node containing a subworkflow.
""" """
def __init__(self, wf_spec, name, subworkflow_spec, transaction=False, **kwargs): def __init__(self, wf_spec, bpmn_id, subworkflow_spec, transaction=False, **kwargs):
""" """
Constructor. Constructor.
:param bpmn_wf_spec: the BpmnProcessSpec for the sub process. :param bpmn_wf_spec: the BpmnProcessSpec for the sub process.
:param bpmn_wf_class: the BpmnWorkflow class to instantiate :param bpmn_wf_class: the BpmnWorkflow class to instantiate
""" """
super(SubWorkflowTask, self).__init__(wf_spec, name, **kwargs) super(SubWorkflowTask, self).__init__(wf_spec, bpmn_id, **kwargs)
self.spec = subworkflow_spec self.spec = subworkflow_spec
self.transaction = transaction self.transaction = transaction
@property
def spec_type(self):
return 'Subprocess'
def _on_subworkflow_completed(self, subworkflow, my_task): def _on_subworkflow_completed(self, subworkflow, my_task):
self.update_data(my_task, subworkflow) self.update_data(my_task, subworkflow)
@ -46,7 +63,7 @@ class SubWorkflowTask(BpmnSpecMixin):
def copy_data(self, my_task, subworkflow): def copy_data(self, my_task, subworkflow):
# There is only one copy of any given data object, so it should be updated immediately # There is only one copy of any given data object, so it should be updated immediately
# Doing this is actually a little problematic, because it gives parent processes access to # Doing this is actually a little problematic, because it gives parent processes access to
# data objects defined in subprocesses. # data objects defined in subprocesses.
# But our data management is already hopelessly messed up and in dire needs of reconsideration # But our data management is already hopelessly messed up and in dire needs of reconsideration
if len(subworkflow.spec.data_objects) > 0: if len(subworkflow.spec.data_objects) > 0:
@ -68,14 +85,11 @@ class SubWorkflowTask(BpmnSpecMixin):
child.task_spec._update(child) child.task_spec._update(child)
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
def task_will_set_children_future(self, my_task):
my_task.workflow.delete_subprocess(my_task)
class CallActivity(SubWorkflowTask): class CallActivity(SubWorkflowTask):
def __init__(self, wf_spec, name, subworkflow_spec, **kwargs): def __init__(self, wf_spec, bpmn_id, subworkflow_spec, **kwargs):
super(CallActivity, self).__init__(wf_spec, name, subworkflow_spec, False, **kwargs) super(CallActivity, self).__init__(wf_spec, bpmn_id, subworkflow_spec, False, **kwargs)
def copy_data(self, my_task, subworkflow): def copy_data(self, my_task, subworkflow):
@ -86,13 +100,13 @@ class CallActivity(SubWorkflowTask):
else: else:
# Otherwise copy only task data with the specified names # Otherwise copy only task data with the specified names
for var in subworkflow.spec.io_specification.data_inputs: for var in subworkflow.spec.io_specification.data_inputs:
if var.name not in my_task.data: if var.bpmn_id not in my_task.data:
raise WorkflowDataException( raise WorkflowDataException(
"You are missing a required Data Input for a call activity.", "You are missing a required Data Input for a call activity.",
task=my_task, task=my_task,
data_input=var, data_input=var,
) )
start[0].data[var.name] = my_task.data[var.name] start[0].data[var.bpmn_id] = my_task.data[var.bpmn_id]
def update_data(self, my_task, subworkflow): def update_data(self, my_task, subworkflow):
@ -103,25 +117,36 @@ class CallActivity(SubWorkflowTask):
end = subworkflow.get_tasks_from_spec_name('End', workflow=subworkflow) end = subworkflow.get_tasks_from_spec_name('End', workflow=subworkflow)
# Otherwise only copy data with the specified names # Otherwise only copy data with the specified names
for var in subworkflow.spec.io_specification.data_outputs: for var in subworkflow.spec.io_specification.data_outputs:
if var.name not in end[0].data: if var.bpmn_id not in end[0].data:
raise WorkflowDataException( raise WorkflowDataException(
f"The Data Output was not available in the subprocess output.", "The Data Output was not available in the subprocess output.",
task=my_task, task=my_task,
data_output=var, data_output=var,
) )
my_task.data[var.name] = end[0].data[var.name] my_task.data[var.bpmn_id] = end[0].data[var.bpmn_id]
@property
def spec_type(self):
return 'Call Activity'
class TransactionSubprocess(SubWorkflowTask): class TransactionSubprocess(SubWorkflowTask):
def __init__(self, wf_spec, name, subworkflow_spec, **kwargs): def __init__(self, wf_spec, bpmn_id, subworkflow_spec, **kwargs):
super(TransactionSubprocess, self).__init__(wf_spec, name, subworkflow_spec, True, **kwargs) super(TransactionSubprocess, self).__init__(wf_spec, bpmn_id, subworkflow_spec, True, **kwargs)
@property
def spec_type(self):
return 'Transactional Subprocess'
def _on_complete_hook(self, my_task):
# It is possible that a transaction could end by throwing an event caught by a boundary event attached to it
# In that case both the subprocess and the boundary event become ready and whichever one gets executed
# first will cancel the other.
# So here I'm checking whether this has happened and cancelling this task in that case.
# I really hate this fix, so I'm only putting it in transactions because that's where I'm having the problem,
# but it's likely to be a general issue that we miraculously haven't run up against.
# We desperately need to get rid of this BonudaryEventParent BS.
parent = my_task.parent
if isinstance(parent.task_spec, _BoundaryEventParent) and len(
[t for t in parent.children if
isinstance(t.task_spec, BoundaryEvent) and
t.task_spec.cancel_activity and
t.state==TaskState.READY
]):
my_task._drop_children()
my_task._set_state(TaskState.CANCELLED)
else:
super()._on_complete_hook(my_task)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,12 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from ...task import TaskState from SpiffWorkflow.task import TaskState
from .BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.specs.Join import Join
from ...specs.Join import Join
class UnstructuredJoin(Join, BpmnSpecMixin): class UnstructuredJoin(Join):
""" """
A helper subclass of Join that makes it work in a slightly friendlier way A helper subclass of Join that makes it work in a slightly friendlier way
for the BPMN style threading for the BPMN style threading
@ -83,13 +82,3 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
task._drop_children() task._drop_children()
else: else:
task.data.update(collected_data) task.data.update(collected_data)
def task_should_set_children_future(self, my_task):
return True
def task_will_set_children_future(self, my_task):
# go find all of the gateways with the same name as this one,
# drop children and set state to WAITING
for t in list(my_task.workflow.task_tree):
if t.task_spec.name == self.name and t.state == TaskState.COMPLETED:
t._set_state(TaskState.WAITING)

View File

@ -0,0 +1,27 @@
# Copyright (C) 2012 Matthew Hampton, 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.specs.base import TaskSpec
class UserTask(TaskSpec):
"""Task Spec for a bpmn:userTask node."""
def __init__(self, wf_spec, bpmn_id, **kwargs):
super().__init__(wf_spec, bpmn_id, **kwargs)
self.manual = True

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2012 Matthew Hampton, 2023 Sartography
# Copyright (C) 2012 Matthew Hampton
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -15,21 +16,26 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import copy import copy
from SpiffWorkflow.bpmn.specs.events.event_definitions import ( from SpiffWorkflow.task import TaskState, Task
from SpiffWorkflow.workflow import Workflow
from SpiffWorkflow.exceptions import WorkflowException, TaskNotFoundException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from SpiffWorkflow.bpmn.specs.mixins.events.event_types import CatchingEvent
from SpiffWorkflow.bpmn.specs.mixins.events.start_event import StartEvent
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import CallActivity
from SpiffWorkflow.bpmn.specs.event_definitions import (
MessageEventDefinition, MessageEventDefinition,
MultipleEventDefinition, MultipleEventDefinition,
NamedEventDefinition, NamedEventDefinition,
TimerEventDefinition, TimerEventDefinition,
) )
from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
from .PythonScriptEngine import PythonScriptEngine from .PythonScriptEngine import PythonScriptEngine
from .specs.events.event_types import CatchingEvent
from .specs.events.StartEvent import StartEvent
from .specs.SubWorkflowTask import CallActivity
from ..task import TaskState, Task
from ..workflow import Workflow
from ..exceptions import TaskNotFoundException, WorkflowException, WorkflowTaskException
class BpmnMessage: class BpmnMessage:
@ -86,7 +92,8 @@ class BpmnWorkflow(Workflow):
def delete_subprocess(self, my_task): def delete_subprocess(self, my_task):
workflow = self._get_outermost_workflow(my_task) workflow = self._get_outermost_workflow(my_task)
del workflow.subprocesses[my_task.id] if my_task.id in workflow.subprocesses:
del workflow.subprocesses[my_task.id]
def get_subprocess(self, my_task): def get_subprocess(self, my_task):
workflow = self._get_outermost_workflow(my_task) workflow = self._get_outermost_workflow(my_task)
@ -94,7 +101,17 @@ class BpmnWorkflow(Workflow):
def connect_subprocess(self, spec_name, name): def connect_subprocess(self, spec_name, name):
# This creates a new task associated with a process when an event that kicks of a process is received # This creates a new task associated with a process when an event that kicks of a process is received
new = CallActivity(self.spec, name, spec_name) # I need to know what class is being used to create new processes in this case, and this seems slightly
# less bad than adding yet another argument. Still sucks though.
# TODO: Make collaborations a class rather than trying to shoehorn them into a process.
for spec in self.spec.task_specs.values():
if isinstance(spec, CallActivity):
spec_class = spec.__class__
break
else:
# Default to the mixin class, which will probably fail in many cases.
spec_class = CallActivity
new = spec_class(self.spec, name, spec_name)
self.spec.start.connect(new) self.spec.start.connect(new)
task = Task(self, new) task = Task(self, new)
start = self.get_tasks_from_spec_name('Start', workflow=self)[0] start = self.get_tasks_from_spec_name('Start', workflow=self)[0]
@ -135,7 +152,7 @@ class BpmnWorkflow(Workflow):
:param event_definition: the thrown event :param event_definition: the thrown event
""" """
# Start a subprocess for known specs with start events that catch this # Start a subprocess for known specs with start events that catch this
# This is total hypocritical of me given how I've argued that specs should # This is totally hypocritical of me given how I've argued that specs should
# be immutable, but I see no other way of doing this. # be immutable, but I see no other way of doing this.
for name, spec in self.subprocess_specs.items(): for name, spec in self.subprocess_specs.items():
for task_spec in list(spec.task_specs.values()): for task_spec in list(spec.task_specs.values()):
@ -183,8 +200,8 @@ class BpmnWorkflow(Workflow):
conversation = task.task_spec.event_definition.conversation() conversation = task.task_spec.event_definition.conversation()
if not conversation: if not conversation:
raise WorkflowTaskException( raise WorkflowTaskException(
f"The waiting task and message payload can not be matched to any correlation key (conversation topic). " "The waiting task and message payload can not be matched to any correlation key (conversation topic). "
f"And is therefor unable to respond to the given message.", task) "And is therefor unable to respond to the given message.", task)
updated_props = self._correlate(conversation, payload, task) updated_props = self._correlate(conversation, payload, task)
task.task_spec.catch(task, event_definition) task.task_spec.catch(task, event_definition)
self.refresh_waiting_tasks() self.refresh_waiting_tasks()
@ -229,7 +246,7 @@ class BpmnWorkflow(Workflow):
elif isinstance(event_definition, MessageEventDefinition): elif isinstance(event_definition, MessageEventDefinition):
value = event_definition.correlation_properties value = event_definition.correlation_properties
events.append({ events.append({
'event_type': event_definition.event_type, 'event_type': event_definition.__class__.__name__,
'name': event_definition.name if isinstance(event_definition, NamedEventDefinition) else None, 'name': event_definition.name if isinstance(event_definition, NamedEventDefinition) else None,
'value': value 'value': value
}) })
@ -246,7 +263,7 @@ class BpmnWorkflow(Workflow):
:param will_complete_task: Callback that will be called prior to completing a task :param will_complete_task: Callback that will be called prior to completing a task
:param did_complete_task: Callback that will be called after completing a task :param did_complete_task: Callback that will be called after completing a task
""" """
engine_steps = list([t for t in self.get_tasks(TaskState.READY) if self._is_engine_task(t.task_spec)]) engine_steps = list([t for t in self.get_tasks(TaskState.READY) if not t.task_spec.manual])
while engine_steps: while engine_steps:
for task in engine_steps: for task in engine_steps:
if will_complete_task is not None: if will_complete_task is not None:
@ -256,7 +273,7 @@ class BpmnWorkflow(Workflow):
did_complete_task(task) did_complete_task(task)
if task.task_spec.name == exit_at: if task.task_spec.name == exit_at:
return task return task
engine_steps = list([t for t in self.get_tasks(TaskState.READY) if self._is_engine_task(t.task_spec)]) engine_steps = list([t for t in self.get_tasks(TaskState.READY) if not t.task_spec.manual])
def refresh_waiting_tasks(self, def refresh_waiting_tasks(self,
will_refresh_task=None, will_refresh_task=None,
@ -292,10 +309,10 @@ class BpmnWorkflow(Workflow):
# almost surely be in a different state than the tasks we want # almost surely be in a different state than the tasks we want
for task in Workflow.get_tasks_iterator(wf): for task in Workflow.get_tasks_iterator(wf):
subprocess = top.subprocesses.get(task.id) subprocess = top.subprocesses.get(task.id)
if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess))
if task._has_state(state): if task._has_state(state):
tasks.append(task) tasks.append(task)
if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess))
return tasks return tasks
def get_task_from_id(self, task_id, workflow=None): def get_task_from_id(self, task_id, workflow=None):
@ -307,12 +324,10 @@ class BpmnWorkflow(Workflow):
def get_ready_user_tasks(self, lane=None, workflow=None): def get_ready_user_tasks(self, lane=None, workflow=None):
"""Returns a list of User Tasks that are READY for user action""" """Returns a list of User Tasks that are READY for user action"""
if lane is not None: if lane is not None:
return [t for t in self.get_tasks(TaskState.READY, workflow) return [t for t in self.get_tasks(TaskState.READY, workflow)
if (not self._is_engine_task(t.task_spec)) if t.task_spec.manual and t.task_spec.lane == lane]
and (t.task_spec.lane == lane)]
else: else:
return [t for t in self.get_tasks(TaskState.READY, workflow) return [t for t in self.get_tasks(TaskState.READY, workflow) if t.task_spec.manual]
if not self._is_engine_task(t.task_spec)]
def get_waiting_tasks(self, workflow=None): def get_waiting_tasks(self, workflow=None):
"""Returns a list of all WAITING tasks""" """Returns a list of all WAITING tasks"""
@ -321,5 +336,58 @@ class BpmnWorkflow(Workflow):
def get_catching_tasks(self, workflow=None): def get_catching_tasks(self, workflow=None):
return [task for task in self.get_tasks(workflow=workflow) if isinstance(task.task_spec, CatchingEvent)] return [task for task in self.get_tasks(workflow=workflow) if isinstance(task.task_spec, CatchingEvent)]
def _is_engine_task(self, task_spec): def reset_from_task_id(self, task_id, data=None):
return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task()) """Override method from base class, and assures that if the task
being reset has a boundary event parent, we reset that parent and
run it rather than resetting to the current task. This assures
our boundary events are set to the correct state."""
task = self.get_task_from_id(task_id)
run_task_at_end = False
if isinstance(task.parent.task_spec, _BoundaryEventParent):
task = task.parent
run_task_at_end = True # we jumped up one level, so exectute so we are on the correct task as requested.
descendants = super().reset_from_task_id(task_id, data)
descendant_ids = [t.id for t in descendants]
top = self._get_outermost_workflow()
delete, reset = [], []
for sp_id, sp in top.subprocesses.items():
if sp_id in descendant_ids:
delete.append(sp_id)
delete.extend([t.id for t in sp.get_tasks() if t.id in top.subprocesses])
if task in sp.get_tasks():
reset.append(sp_id)
# Remove any subprocesses for removed tasks
for sp_id in delete:
del top.subprocesses[sp_id]
# Reset any containing subprocesses
for sp_id in reset:
descendants.extend(self.reset_from_task_id(sp_id))
sp_task = self.get_task_from_id(sp_id)
sp_task.state = TaskState.WAITING
if run_task_at_end:
task.run()
return descendants
def cancel(self, workflow=None):
wf = workflow or self
cancelled = Workflow.cancel(wf)
cancelled_ids = [t.id for t in cancelled]
top = self._get_outermost_workflow()
to_cancel = []
for sp_id, sp in top.subprocesses.items():
if sp_id in cancelled_ids:
to_cancel.append(sp)
for sp in to_cancel:
cancelled.extend(self.cancel(sp))
return cancelled

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,14 +1,40 @@
# Copyright (C) 2023 Sartography
from SpiffWorkflow.bpmn.parser.BpmnParser import full_tag, DEFAULT_NSMAP #
# This file is part of SpiffWorkflow.
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask #
from SpiffWorkflow.bpmn.specs.NoneTask import NoneTask # SpiffWorkflow is free software; you can redistribute it and/or
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask # modify it under the terms of the GNU Lesser General Public
from SpiffWorkflow.bpmn.specs.SubWorkflowTask import CallActivity, TransactionSubprocess # License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask from SpiffWorkflow.bpmn.parser.BpmnParser import full_tag, DEFAULT_NSMAP
from SpiffWorkflow.camunda.specs.UserTask import UserTask
from SpiffWorkflow.bpmn.specs.defaults import (
ManualTask,
NoneTask,
ScriptTask,
CallActivity,
TransactionSubprocess,
StartEvent,
EndEvent,
IntermediateThrowEvent,
IntermediateCatchEvent,
BoundaryEvent
)
from SpiffWorkflow.camunda.specs.business_rule_task import BusinessRuleTask
from SpiffWorkflow.camunda.specs.user_task import UserTask
from SpiffWorkflow.camunda.parser.task_spec import ( from SpiffWorkflow.camunda.parser.task_spec import (
CamundaTaskParser, CamundaTaskParser,
BusinessRuleTaskParser, BusinessRuleTaskParser,
@ -18,10 +44,6 @@ from SpiffWorkflow.camunda.parser.task_spec import (
ScriptTaskParser, ScriptTaskParser,
CAMUNDA_MODEL_NS CAMUNDA_MODEL_NS
) )
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent
from .event_parsers import ( from .event_parsers import (
CamundaStartEventParser, CamundaStartEventParser,
CamundaEndEventParser, CamundaEndEventParser,

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,7 +1,31 @@
from SpiffWorkflow.bpmn.parser.event_parsers import EventDefinitionParser # Copyright (C) 2023 Sartography
from SpiffWorkflow.bpmn.parser.event_parsers import StartEventParser, EndEventParser, \ #
IntermediateCatchEventParser, IntermediateThrowEventParser, BoundaryEventParser # This file is part of SpiffWorkflow.
from SpiffWorkflow.camunda.specs.events.event_definitions import MessageEventDefinition #
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.parser.event_parsers import (
EventDefinitionParser,
StartEventParser,
EndEventParser,
IntermediateCatchEventParser,
IntermediateThrowEventParser,
BoundaryEventParser
)
from SpiffWorkflow.camunda.specs.event_definitions import MessageEventDefinition
from SpiffWorkflow.bpmn.parser.util import one from SpiffWorkflow.bpmn.parser.util import one

View File

@ -1,4 +1,21 @@
from ...camunda.specs.UserTask import Form, FormField, EnumFormField # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference
from SpiffWorkflow.bpmn.parser.util import one from SpiffWorkflow.bpmn.parser.util import one
@ -6,9 +23,9 @@ from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser
from SpiffWorkflow.bpmn.parser.task_parsers import SubprocessParser from SpiffWorkflow.bpmn.parser.task_parsers import SubprocessParser
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask from SpiffWorkflow.camunda.specs.business_rule_task import BusinessRuleTask
from SpiffWorkflow.camunda.specs.multiinstance_task import SequentialMultiInstanceTask, ParallelMultiInstanceTask from SpiffWorkflow.camunda.specs.multiinstance_task import SequentialMultiInstanceTask, ParallelMultiInstanceTask
from SpiffWorkflow.camunda.specs.user_task import Form, FormField, EnumFormField
CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn' CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn'
@ -66,11 +83,9 @@ class BusinessRuleTaskParser(CamundaTaskParser):
def create_task(self): def create_task(self):
decision_ref = self.get_decision_ref(self.node) decision_ref = self.get_decision_ref(self.node)
return BusinessRuleTask(self.spec, self.get_task_spec_name(), return BusinessRuleTask(self.spec, self.bpmn_id,
dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node), dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node),
lane=self.lane, position=self.position, **self.bpmn_attributes)
description=self.node.get('name', None),
)
@staticmethod @staticmethod
def get_decision_ref(node): def get_decision_ref(node):
@ -82,10 +97,7 @@ class UserTaskParser(CamundaTaskParser):
def create_task(self): def create_task(self):
form = self.get_form() form = self.get_form()
return self.spec_class(self.spec, self.get_task_spec_name(), form, return self.spec_class(self.spec, self.bpmn_id, form=form, **self.bpmn_attributes)
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
def get_form(self): def get_form(self):
"""Camunda provides a simple form builder, this will extract the """Camunda provides a simple form builder, this will extract the
@ -138,10 +150,7 @@ class SubWorkflowParser(CamundaTaskParser):
def create_task(self): def create_task(self):
subworkflow_spec = SubprocessParser.get_subprocess_spec(self) subworkflow_spec = SubprocessParser.get_subprocess_spec(self)
return self.spec_class( return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
class CallActivityParser(CamundaTaskParser): class CallActivityParser(CamundaTaskParser):
@ -149,10 +158,7 @@ class CallActivityParser(CamundaTaskParser):
def create_task(self): def create_task(self):
subworkflow_spec = SubprocessParser.get_call_activity_spec(self) subworkflow_spec = SubprocessParser.get_call_activity_spec(self)
return self.spec_class( return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
class ScriptTaskParser(TaskParser): class ScriptTaskParser(TaskParser):
@ -162,10 +168,7 @@ class ScriptTaskParser(TaskParser):
def create_task(self): def create_task(self):
script = self.get_script() script = self.get_script()
return self.spec_class(self.spec, self.get_task_spec_name(), script, return self.spec_class(self.spec, self.bpmn_id, script=script, **self.bpmn_attributes)
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
def get_script(self): def get_script(self):
""" """
@ -177,5 +180,5 @@ class ScriptTaskParser(TaskParser):
return one(self.xpath('.//bpmn:script')).text return one(self.xpath('.//bpmn:script')).text
except AssertionError as ae: except AssertionError as ae:
raise ValidationException( raise ValidationException(
f"Invalid Script Task. No Script Provided. " + str(ae), "Invalid Script Task. No Script Provided. " + str(ae),
node=self.node, file_name=self.filename) node=self.node, file_name=self.filename)

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from copy import deepcopy from copy import deepcopy
from SpiffWorkflow.bpmn.serializer.workflow import DEFAULT_SPEC_CONFIG from SpiffWorkflow.bpmn.serializer.workflow import DEFAULT_SPEC_CONFIG
@ -8,8 +27,12 @@ from SpiffWorkflow.bpmn.serializer.task_spec import (
) )
from SpiffWorkflow.bpmn.serializer.event_definition import MessageEventDefinitionConverter as DefaultMessageEventConverter from SpiffWorkflow.bpmn.serializer.event_definition import MessageEventDefinitionConverter as DefaultMessageEventConverter
from .task_spec import (
from .task_spec import UserTaskConverter, ParallelMultiInstanceTaskConverter, SequentialMultiInstanceTaskConverter UserTaskConverter,
BusinessRuleTaskConverter,
ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter
)
from .event_definition import MessageEventDefinitionConverter from .event_definition import MessageEventDefinitionConverter
@ -20,6 +43,7 @@ CAMUNDA_SPEC_CONFIG['task_specs'].remove(DefaultParallelMIConverter)
CAMUNDA_SPEC_CONFIG['task_specs'].append(ParallelMultiInstanceTaskConverter) CAMUNDA_SPEC_CONFIG['task_specs'].append(ParallelMultiInstanceTaskConverter)
CAMUNDA_SPEC_CONFIG['task_specs'].remove(DefaultSequentialMIConverter) CAMUNDA_SPEC_CONFIG['task_specs'].remove(DefaultSequentialMIConverter)
CAMUNDA_SPEC_CONFIG['task_specs'].append(SequentialMultiInstanceTaskConverter) CAMUNDA_SPEC_CONFIG['task_specs'].append(SequentialMultiInstanceTaskConverter)
CAMUNDA_SPEC_CONFIG['task_specs'].append(BusinessRuleTaskConverter)
CAMUNDA_SPEC_CONFIG['event_definitions'].remove(DefaultMessageEventConverter) CAMUNDA_SPEC_CONFIG['event_definitions'].remove(DefaultMessageEventConverter)
CAMUNDA_SPEC_CONFIG['event_definitions'].append(MessageEventDefinitionConverter) CAMUNDA_SPEC_CONFIG['event_definitions'].append(MessageEventDefinitionConverter)

View File

@ -1,5 +1,24 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.serializer.helpers.spec import EventDefinitionConverter from SpiffWorkflow.bpmn.serializer.helpers.spec import EventDefinitionConverter
from ..specs.events.event_definitions import MessageEventDefinition from ..specs.event_definitions import MessageEventDefinition
class MessageEventDefinitionConverter(EventDefinitionConverter): class MessageEventDefinitionConverter(EventDefinitionConverter):

View File

@ -1,7 +1,28 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.serializer.helpers.spec import TaskSpecConverter from SpiffWorkflow.bpmn.serializer.helpers.spec import TaskSpecConverter
from SpiffWorkflow.bpmn.serializer.task_spec import MultiInstanceTaskConverter from SpiffWorkflow.bpmn.serializer.task_spec import MultiInstanceTaskConverter
from SpiffWorkflow.dmn.serializer.task_spec import BaseBusinessRuleTaskConverter
from SpiffWorkflow.camunda.specs.UserTask import UserTask, Form from SpiffWorkflow.camunda.specs.user_task import UserTask, Form
from SpiffWorkflow.camunda.specs.business_rule_task import BusinessRuleTask
from SpiffWorkflow.camunda.specs.multiinstance_task import ParallelMultiInstanceTask, SequentialMultiInstanceTask from SpiffWorkflow.camunda.specs.multiinstance_task import ParallelMultiInstanceTask, SequentialMultiInstanceTask
class UserTaskConverter(TaskSpecConverter): class UserTaskConverter(TaskSpecConverter):
@ -11,7 +32,6 @@ class UserTaskConverter(TaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['form'] = self.form_to_dict(spec.form) dct['form'] = self.form_to_dict(spec.form)
return dct return dct
@ -36,6 +56,10 @@ class UserTaskConverter(TaskSpecConverter):
return dct return dct
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
class ParallelMultiInstanceTaskConverter(MultiInstanceTaskConverter): class ParallelMultiInstanceTaskConverter(MultiInstanceTaskConverter):
def __init__(self, registry): def __init__(self, registry):
super().__init__(ParallelMultiInstanceTask, registry) super().__init__(ParallelMultiInstanceTask, registry)

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -0,0 +1,5 @@
from SpiffWorkflow.dmn.specs.business_rule_task_mixin import BusinessRuleTaskMixin
from SpiffWorkflow.bpmn.specs.mixins.bpmn_spec_mixin import BpmnSpecMixin
class BusinessRuleTask(BusinessRuleTaskMixin, BpmnSpecMixin):
pass

View File

@ -1,4 +1,23 @@
from SpiffWorkflow.bpmn.specs.events.event_definitions import MessageEventDefinition # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.specs.event_definitions import MessageEventDefinition
class MessageEventDefinition(MessageEventDefinition): class MessageEventDefinition(MessageEventDefinition):
""" """
@ -10,9 +29,9 @@ class MessageEventDefinition(MessageEventDefinition):
# this should be revisited: for one thing, we're relying on some Camunda-specific # this should be revisited: for one thing, we're relying on some Camunda-specific
# properties. # properties.
def __init__(self, name, correlation_properties=None, payload=None, result_var=None): def __init__(self, name, correlation_properties=None, payload=None, result_var=None, **kwargs):
super(MessageEventDefinition, self).__init__(name, correlation_properties) super(MessageEventDefinition, self).__init__(name, correlation_properties, **kwargs)
self.payload = payload self.payload = payload
self.result_var = result_var self.result_var = result_var

View File

@ -1,8 +1,27 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.specs.BpmnSpecMixin import BpmnSpecMixin from SpiffWorkflow.bpmn.specs.mixins.bpmn_spec_mixin import BpmnSpecMixin
from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference from SpiffWorkflow.bpmn.specs.data_spec import TaskDataReference
from SpiffWorkflow.bpmn.specs.MultiInstanceTask import ( from SpiffWorkflow.bpmn.specs.defaults import (
SequentialMultiInstanceTask as BpmnSequentialMITask, SequentialMultiInstanceTask as BpmnSequentialMITask,
ParallelMultiInstanceTask as BpmnParallelMITask, ParallelMultiInstanceTask as BpmnParallelMITask,
) )
@ -20,19 +39,19 @@ def update_task_spec(my_task):
if task_spec.cardinality is None: if task_spec.cardinality is None:
# Use the same collection for input and output # Use the same collection for input and output
task_spec.data_input = TaskDataReference(task_spec.data_output.name) task_spec.data_input = TaskDataReference(task_spec.data_output.bpmn_id)
task_spec.input_item = TaskDataReference(task_spec.output_item.name) task_spec.input_item = TaskDataReference(task_spec.output_item.bpmn_id)
else: else:
cardinality = my_task.workflow.script_engine.evaluate(my_task, task_spec.cardinality) cardinality = my_task.workflow.script_engine.evaluate(my_task, task_spec.cardinality)
if not isinstance(cardinality, int): if not isinstance(cardinality, int):
# The input data was supplied via "cardinality" # The input data was supplied via "cardinality"
# We'll use the same reference for input and output item # We'll use the same reference for input and output item
task_spec.data_input = TaskDataReference(task_spec.cardinality) task_spec.data_input = TaskDataReference(task_spec.cardinality)
task_spec.input_item = TaskDataReference(task_spec.output_item.name) if task_spec.output_item is not None else None task_spec.input_item = TaskDataReference(task_spec.output_item.bpmn_id) if task_spec.output_item is not None else None
task_spec.cardinality = None task_spec.cardinality = None
else: else:
# This will be the index # This will be the index
task_spec.input_item = TaskDataReference(task_spec.output_item.name) if task_spec.output_item is not None else None task_spec.input_item = TaskDataReference(task_spec.output_item.bpmn_id) if task_spec.output_item is not None else None
class SequentialMultiInstanceTask(BpmnSequentialMITask): class SequentialMultiInstanceTask(BpmnSequentialMITask):

View File

@ -1,11 +1,26 @@
# -*- coding: utf-8 -*- # Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.bpmn.specs.defaults import UserTask as DefaultUserTask
from ...bpmn.specs.UserTask import UserTask class UserTask(DefaultUserTask):
from ...bpmn.specs.BpmnSpecMixin import BpmnSpecMixin
class UserTask(UserTask, BpmnSpecMixin):
"""Task Spec for a bpmn:userTask node with Camunda forms.""" """Task Spec for a bpmn:userTask node with Camunda forms."""
def __init__(self, wf_spec, name, form, **kwargs): def __init__(self, wf_spec, name, form, **kwargs):
@ -17,12 +32,6 @@ class UserTask(UserTask, BpmnSpecMixin):
super(UserTask, self).__init__(wf_spec, name, **kwargs) super(UserTask, self).__init__(wf_spec, name, **kwargs)
self.form = form self.form = form
def _on_trigger(self, my_task):
pass
def is_engine_task(self):
return False
class FormField(object): class FormField(object):
def __init__(self, form_type="text"): def __init__(self, form_type="text"):

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,10 +1,29 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import logging import logging
import re import re
from SpiffWorkflow.exceptions import SpiffWorkflowException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from ..specs.model import HitPolicy from ..specs.model import HitPolicy
from ...exceptions import SpiffWorkflowException, WorkflowTaskException
from ...util import levenshtein
from ...workflow import WorkflowException
logger = logging.getLogger('spiff.dmn') logger = logging.getLogger('spiff.dmn')
@ -37,7 +56,7 @@ class DMNEngine:
for rule in matched_rules: for rule in matched_rules:
rule_output = rule.output_as_dict(task) rule_output = rule.output_as_dict(task)
for key in rule_output.keys(): for key in rule_output.keys():
if not key in result: if key not in result:
result[key] = [] result[key] = []
result[key].append(rule_output[key]) result[key].append(rule_output[key])
elif len(matched_rules) > 0: elif len(matched_rules) > 0:
@ -98,7 +117,7 @@ class DMNEngine:
# NOTE: It should only do this replacement outside of quotes. # NOTE: It should only do this replacement outside of quotes.
# for example, provided "This thing?" in quotes, it should not # for example, provided "This thing?" in quotes, it should not
# do the replacement. # do the replacement.
match_expr = re.sub('(\?)(?=(?:[^\'"]|[\'"][^\'"]*[\'"])*$)', 'dmninputexpr', match_expr) match_expr = re.sub(r'(\?)(?=(?:[^\'"]|[\'"][^\'"]*[\'"])*$)', 'dmninputexpr', match_expr)
if 'dmninputexpr' in match_expr: if 'dmninputexpr' in match_expr:
external_methods = { external_methods = {
'dmninputexpr': script_engine.evaluate(task, input_expr) 'dmninputexpr': script_engine.evaluate(task, input_expr)

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import glob import glob
import os import os
@ -53,7 +72,7 @@ class BpmnDmnParser(BpmnParser):
validator.validate(node, filename) validator.validate(node, filename)
dmn_parser = DMNParser(self, node, nsmap, filename=filename) dmn_parser = DMNParser(self, node, nsmap, filename=filename)
self.dmn_parsers[dmn_parser.get_id()] = dmn_parser self.dmn_parsers[dmn_parser.bpmn_id] = dmn_parser
self.dmn_parsers_by_name[dmn_parser.get_name()] = dmn_parser self.dmn_parsers_by_name[dmn_parser.get_name()] = dmn_parser
def add_dmn_file(self, filename): def add_dmn_file(self, filename):
@ -75,7 +94,19 @@ class BpmnDmnParser(BpmnParser):
""" """
for filename in filenames: for filename in filenames:
with open(filename, 'r') as f: with open(filename, 'r') as f:
self.add_dmn_xml(etree.parse(f).getroot(), filename=filename) self.add_dmn_io(f, filename=filename)
def add_dmn_io(self, file_like_object, filename=None):
"""
Add the given DMN file like object to the parser's set.
"""
self.add_dmn_xml(etree.parse(file_like_object).getroot(), filename)
def add_dmn_str(self, dmn_str, filename=None):
"""
Add the given DMN string to the parser's set.
"""
self.add_dmn_xml(etree.fromstring(dmn_str), filename)
def get_dependencies(self): def get_dependencies(self):
return self.process_dependencies.union(self.dmn_dependencies) return self.process_dependencies.union(self.dmn_dependencies)

View File

@ -1,12 +1,38 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import ast import ast
from SpiffWorkflow.bpmn.parser.node_parser import NodeParser, DEFAULT_NSMAP from SpiffWorkflow.bpmn.parser.node_parser import NodeParser, DEFAULT_NSMAP
from ...bpmn.parser.ValidationException import ValidationException from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from ...bpmn.parser.util import xpath_eval from SpiffWorkflow.bpmn.parser.util import xpath_eval
from ...dmn.specs.model import Decision, DecisionTable, InputEntry, \ from SpiffWorkflow.dmn.specs.model import (
OutputEntry, Input, Output, Rule Decision,
DecisionTable,
InputEntry,
OutputEntry,
Input,
Output,
Rule,
)
def get_dmn_ns(node): def get_dmn_ns(node):
""" """
@ -55,7 +81,8 @@ class DMNParser(NodeParser):
def parse(self): def parse(self):
self.decision = self._parse_decision(self.node.findall('{*}decision')) self.decision = self._parse_decision(self.node.findall('{*}decision'))
def get_id(self): @property
def bpmn_id(self):
""" """
Returns the process ID Returns the process ID
""" """
@ -172,9 +199,7 @@ class DMNParser(NodeParser):
return rule return rule
def _parse_input_output_element(self, decision_table, element, cls, idx): def _parse_input_output_element(self, decision_table, element, cls, idx):
input_or_output = ( input_or_output = (decision_table.inputs if cls == InputEntry else decision_table.outputs)[idx]
decision_table.inputs if cls == InputEntry else decision_table.outputs if cls == OutputEntry else None)[
idx]
entry = cls(element.attrib['id'], input_or_output) entry = cls(element.attrib['id'], input_or_output)
for child in element: for child in element:
if child.tag.endswith('description'): if child.tag.endswith('description'):
@ -182,7 +207,8 @@ class DMNParser(NodeParser):
elif child.tag.endswith('text'): elif child.tag.endswith('text'):
entry.text = child.text entry.text = child.text
if cls == InputEntry: if cls == InputEntry:
entry.lhs.append(entry.text) # DMN renders 'no input specification' with '-'; assume this is intended if somebody has added '-'
entry.lhs.append(entry.text if entry.text != '-' else None)
elif cls == OutputEntry: elif cls == OutputEntry:
if entry.text and entry.text != '': if entry.text and entry.text != '':
try: try:

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,6 +1,24 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ...bpmn.serializer.helpers.spec import TaskSpecConverter from ...bpmn.serializer.helpers.spec import TaskSpecConverter
from ..specs.BusinessRuleTask import BusinessRuleTask
from ..specs.model import DecisionTable, Rule, HitPolicy from ..specs.model import DecisionTable, Rule, HitPolicy
from ..specs.model import Input, InputEntry, Output, OutputEntry from ..specs.model import Input, InputEntry, Output, OutputEntry
from ..engine.DMNEngine import DMNEngine from ..engine.DMNEngine import DMNEngine
@ -9,7 +27,6 @@ class BaseBusinessRuleTaskConverter(TaskSpecConverter):
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
# We only ever use one decision table # We only ever use one decision table
dct['decision_table'] = self.decision_table_to_dict(spec.dmnEngine.decision_table) dct['decision_table'] = self.decision_table_to_dict(spec.dmnEngine.decision_table)
return dct return dct
@ -95,8 +112,3 @@ class BaseBusinessRuleTaskConverter(TaskSpecConverter):
rule.outputEntries = [self.output_entry_from_dict(entry, outputs) rule.outputEntries = [self.output_entry_from_dict(entry, outputs)
for entry in dct['output_entries']] for entry in dct['output_entries']]
return rule return rule
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)

View File

@ -1,37 +0,0 @@
from SpiffWorkflow.exceptions import WorkflowTaskException, SpiffWorkflowException
from ...specs.Simple import Simple
from ...bpmn.specs.BpmnSpecMixin import BpmnSpecMixin
from ...util.deep_merge import DeepMerge
class BusinessRuleTask(Simple, BpmnSpecMixin):
"""
Task Spec for a bpmn:businessTask (DMB Decision Reference) node.
"""
def _on_trigger(self, my_task):
pass
def __init__(self, wf_spec, name, dmnEngine, **kwargs):
super().__init__(wf_spec, name, **kwargs)
self.dmnEngine = dmnEngine
self.resDict = None
@property
def spec_class(self):
return 'Business Rule Task'
def _run_hook(self, my_task):
try:
my_task.data = DeepMerge.merge(my_task.data, self.dmnEngine.result(my_task))
super(BusinessRuleTask, self)._run_hook(my_task)
except SpiffWorkflowException as we:
we.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise we
except Exception as e:
error = WorkflowTaskException(str(e), task=my_task)
error.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise error
return True

View File

@ -0,0 +1,18 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -0,0 +1,49 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.exceptions import SpiffWorkflowException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from SpiffWorkflow.specs.base import TaskSpec
from SpiffWorkflow.util.deep_merge import DeepMerge
class BusinessRuleTaskMixin(TaskSpec):
"""Task Spec for a bpmn:businessTask (DMB Decision Reference) node."""
def __init__(self, wf_spec, name, dmnEngine, **kwargs):
super().__init__(wf_spec, name, **kwargs)
self.dmnEngine = dmnEngine
self.resDict = None
@property
def spec_class(self):
return 'Business Rule Task'
def _run_hook(self, my_task):
try:
my_task.data = DeepMerge.merge(my_task.data, self.dmnEngine.result(my_task))
super(BusinessRuleTaskMixin, self)._run_hook(my_task)
except SpiffWorkflowException as we:
we.add_note(f"Business Rule Task '{my_task.task_spec.bpmn_name}'.")
raise we
except Exception as e:
error = WorkflowTaskException(str(e), task=my_task)
error.add_note(f"Business Rule Task '{my_task.task_spec.bpmn_name}'.")
raise error
return True

View File

@ -1,3 +1,22 @@
# Copyright (C) 2023 Sartography
#
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from collections import OrderedDict from collections import OrderedDict
from enum import Enum from enum import Enum

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2007 Samuel Abels, 2023 Sartography
# Copyright (C) 2007 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -15,10 +16,6 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import re
from SpiffWorkflow.util import levenshtein
class SpiffWorkflowException(Exception): class SpiffWorkflowException(Exception):
""" """
@ -55,81 +52,6 @@ class WorkflowException(SpiffWorkflowException):
# Points to the TaskSpec that generated the exception. # Points to the TaskSpec that generated the exception.
self.task_spec = task_spec self.task_spec = task_spec
@staticmethod
def get_task_trace(task):
task_trace = [f"{task.task_spec.description} ({task.workflow.spec.file})"]
workflow = task.workflow
while workflow != workflow.outer_workflow:
caller = workflow.name
workflow = workflow.outer_workflow
task_trace.append(f"{workflow.spec.task_specs[caller].description} ({workflow.spec.file})")
return task_trace
@staticmethod
def did_you_mean_from_name_error(name_exception, options):
"""Returns a string along the lines of 'did you mean 'dog'? Given
a name_error, and a set of possible things that could have been called,
or an empty string if no valid suggestions come up. """
if isinstance(name_exception, NameError):
def_match = re.match("name '(.+)' is not defined", str(name_exception))
if def_match:
bad_variable = re.match("name '(.+)' is not defined",
str(name_exception)).group(1)
most_similar = levenshtein.most_similar(bad_variable,
options, 3)
error_msg = ""
if len(most_similar) == 1:
error_msg += f' Did you mean \'{most_similar[0]}\'?'
if len(most_similar) > 1:
error_msg += f' Did you mean one of \'{most_similar}\'?'
return error_msg
class WorkflowTaskException(WorkflowException):
"""WorkflowException that provides task_trace information."""
def __init__(self, error_msg, task=None, exception=None,
line_number=None, offset=None, error_line=None):
"""
Exception initialization.
:param task: the task that threw the exception
:type task: Task
:param error_msg: a human readable error message
:type error_msg: str
:param exception: an exception to wrap, if any
:type exception: Exception
"""
self.task = task
self.line_number = line_number
self.offset = offset
self.error_line = error_line
if exception:
self.error_type = exception.__class__.__name__
else:
self.error_type = "unknown"
super().__init__(error_msg, task_spec=task.task_spec)
if isinstance(exception, SyntaxError) and not line_number:
# Line number and offset can be recovered directly from syntax errors,
# otherwise they must be passed in.
self.line_number = exception.lineno
self.offset = exception.offset
elif isinstance(exception, NameError):
self.add_note(self.did_you_mean_from_name_error(exception, list(task.data.keys())))
# If encountered in a sub-workflow, this traces back up the stack,
# so we can tell how we got to this particular task, no matter how
# deeply nested in sub-workflows it is. Takes the form of:
# task-description (file-name)
self.task_trace = self.get_task_trace(task)
class StorageException(SpiffWorkflowException):
pass
class TaskNotFoundException(WorkflowException): class TaskNotFoundException(WorkflowException):
pass pass

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007 Samuel Abels # Copyright (C) 2007 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,6 +16,7 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import logging import logging
import re import re
@ -198,8 +199,8 @@ def valueof(scope, op, default=None):
def is_number(text): def is_number(text):
try: try:
x = int(text) int(text)
except: except Exception:
return False return False
return True return True

View File

@ -0,0 +1,16 @@
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

View File

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*- # This file is part of SpiffWorkflow.
#
from builtins import object # SpiffWorkflow is free software; you can redistribute it and/or
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -20,7 +19,6 @@ from .. import operators
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute from ..specs.Execute import Execute
@ -46,7 +44,6 @@ def spec_map():
'acquire-mutex': AcquireMutex, 'acquire-mutex': AcquireMutex,
'cancel': Cancel, 'cancel': Cancel,
'cancel-task': CancelTask, 'cancel-task': CancelTask,
'celery': Celery,
'choose': Choose, 'choose': Choose,
'exclusive-choice': ExclusiveChoice, 'exclusive-choice': ExclusiveChoice,
'execute': Execute, 'execute': Execute,

View File

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*- # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
import json
from builtins import str
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -17,17 +14,19 @@ from builtins import str
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import json
import pickle import pickle
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from ..workflow import Workflow from ..workflow import Workflow
from ..util.impl import get_class from ..util.impl import get_class
from ..task import Task, TaskState from ..task import Task, TaskState
from ..operators import (Attrib, PathAttrib, Equal, NotEqual, Operator, GreaterThan, LessThan, Match) from ..operators import Attrib, PathAttrib, Equal, NotEqual, Operator, GreaterThan, LessThan, Match
from ..specs.base import TaskSpec from ..specs.base import TaskSpec
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute from ..specs.Execute import Execute
@ -51,25 +50,17 @@ import warnings
class DictionarySerializer(Serializer): class DictionarySerializer(Serializer):
def __init__(self):
# When deserializing, this is a set of specs for sub-workflows.
# This prevents us from serializing a copy of the same spec many
# times, which can create very large files.
self.SPEC_STATES = {}
def serialize_dict(self, thedict): def serialize_dict(self, thedict):
return dict( return dict(
(str(k), b64encode(pickle.dumps(v, (str(k), b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)))
protocol=pickle.HIGHEST_PROTOCOL))) for k, v in list(thedict.items())
for k, v in list(thedict.items())) )
def deserialize_dict(self, s_state): def deserialize_dict(self, s_state):
return dict((k, pickle.loads(b64decode(v))) return dict((k, pickle.loads(b64decode(v))) for k, v in list(s_state.items()))
for k, v in list(s_state.items()))
def serialize_list(self, thelist): def serialize_list(self, thelist):
return [b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)) return [b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)) for v in thelist]
for v in thelist]
def deserialize_list(self, s_state): def deserialize_list(self, s_state):
return [pickle.loads(b64decode(v)) for v in s_state] return [pickle.loads(b64decode(v)) for v in s_state]
@ -149,48 +140,34 @@ class DictionarySerializer(Serializer):
return ret return ret
def serialize_task_spec(self, spec): def serialize_task_spec(self, spec):
s_state = dict(id=spec.id, s_state = dict(name=spec.name,
name=spec.name,
description=spec.description, description=spec.description,
manual=spec.manual, manual=spec.manual,
internal=spec.internal,
lookahead=spec.lookahead) lookahead=spec.lookahead)
module_name = spec.__class__.__module__ module_name = spec.__class__.__module__
s_state['class'] = module_name + '.' + spec.__class__.__name__ s_state['class'] = module_name + '.' + spec.__class__.__name__
s_state['inputs'] = [t.id for t in spec.inputs] s_state['inputs'] = [t.name for t in spec.inputs]
s_state['outputs'] = [t.id for t in spec.outputs] s_state['outputs'] = [t.name for t in spec.outputs]
s_state['data'] = self.serialize_dict(spec.data) s_state['data'] = self.serialize_dict(spec.data)
if hasattr(spec, 'position'):
s_state['position'] = self.serialize_dict(spec.position)
s_state['defines'] = self.serialize_dict(spec.defines) s_state['defines'] = self.serialize_dict(spec.defines)
s_state['pre_assign'] = self.serialize_list(spec.pre_assign) s_state['pre_assign'] = self.serialize_list(spec.pre_assign)
s_state['post_assign'] = self.serialize_list(spec.post_assign) s_state['post_assign'] = self.serialize_list(spec.post_assign)
# Note: Events are not serialized; this is documented in # Note: Events are not serialized; this is documented in
# the TaskSpec API docs. # the TaskSpec API docs.
return s_state return s_state
def deserialize_task_spec(self, wf_spec, s_state, spec): def deserialize_task_spec(self, wf_spec, s_state, spec):
spec.id = s_state.get('id', None)
spec.description = s_state.get('description', '') spec.description = s_state.get('description', '')
spec.manual = s_state.get('manual', False) spec.manual = s_state.get('manual', False)
spec.internal = s_state.get('internal', False)
spec.lookahead = s_state.get('lookahead', 2) spec.lookahead = s_state.get('lookahead', 2)
spec.data = self.deserialize_dict(s_state.get('data', {})) spec.data = self.deserialize_dict(s_state.get('data', {}))
if 'position' in s_state.keys():
spec.position = self.deserialize_dict(s_state.get('position', {}))
spec.defines = self.deserialize_dict(s_state.get('defines', {})) spec.defines = self.deserialize_dict(s_state.get('defines', {}))
spec.pre_assign = self.deserialize_list(s_state.get('pre_assign', [])) spec.pre_assign = self.deserialize_list(s_state.get('pre_assign', []))
spec.post_assign = self.deserialize_list( spec.post_assign = self.deserialize_list(s_state.get('post_assign', []))
s_state.get('post_assign', []))
# We can't restore inputs and outputs yet because they may not be # We can't restore inputs and outputs yet because they may not be
# deserialized yet. So keep the names, and resolve them in the end. # deserialized yet. So keep the names, and resolve them in the end.
spec.inputs = s_state.get('inputs', [])[:] spec.inputs = s_state.get('inputs', [])[:]
spec.outputs = s_state.get('outputs', [])[:] spec.outputs = s_state.get('outputs', [])[:]
return spec return spec
def serialize_acquire_mutex(self, spec): def serialize_acquire_mutex(self, spec):
@ -210,8 +187,7 @@ class DictionarySerializer(Serializer):
return s_state return s_state
def deserialize_cancel(self, wf_spec, s_state): def deserialize_cancel(self, wf_spec, s_state):
spec = Cancel(wf_spec, s_state['name'], spec = Cancel(wf_spec, s_state['name'], success=s_state.get('cancel_successfully', False))
success=s_state.get('cancel_successfully', False))
self.deserialize_task_spec(wf_spec, s_state, spec=spec) self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec return spec
@ -226,26 +202,6 @@ class DictionarySerializer(Serializer):
self.deserialize_task_spec(wf_spec, s_state, spec=spec) self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec return spec
def serialize_celery(self, spec):
args = self.serialize_list(spec.args)
kwargs = self.serialize_dict(spec.kwargs)
s_state = self.serialize_task_spec(spec)
s_state['call'] = spec.call
s_state['args'] = args
s_state['kwargs'] = kwargs
s_state['result_key'] = spec.result_key
return s_state
def deserialize_celery(self, wf_spec, s_state):
args = self.deserialize_list(s_state['args'])
kwargs = self.deserialize_dict(s_state.get('kwargs', {}))
spec = Celery(wf_spec, s_state['name'], s_state['call'],
call_args=args,
result_key=s_state['result_key'],
**kwargs)
self.deserialize_task_spec(wf_spec, s_state, spec)
return spec
def serialize_choose(self, spec): def serialize_choose(self, spec):
s_state = self.serialize_task_spec(spec) s_state = self.serialize_task_spec(spec)
s_state['context'] = spec.context s_state['context'] = spec.context
@ -305,12 +261,12 @@ class DictionarySerializer(Serializer):
s_state['cancel_remaining'] = spec.cancel_remaining s_state['cancel_remaining'] = spec.cancel_remaining
return s_state return s_state
def deserialize_join(self, wf_spec, s_state, cls=Join): def deserialize_join(self, wf_spec, s_state):
if isinstance(s_state['threshold'],dict): if isinstance(s_state['threshold'],dict):
byte_payload = s_state['threshold']['__bytes__'] byte_payload = s_state['threshold']['__bytes__']
else: else:
byte_payload = s_state['threshold'] byte_payload = s_state['threshold']
spec = cls(wf_spec, spec = Join(wf_spec,
s_state['name'], s_state['name'],
split_task=s_state['split_task'], split_task=s_state['split_task'],
threshold=pickle.loads(b64decode(byte_payload)), threshold=pickle.loads(b64decode(byte_payload)),
@ -347,36 +303,25 @@ class DictionarySerializer(Serializer):
s_state = self.serialize_task_spec(spec) s_state = self.serialize_task_spec(spec)
# here we need to add in all of the things that would get serialized # here we need to add in all of the things that would get serialized
# for other classes that the MultiInstance could be - # for other classes that the MultiInstance could be -
#
if isinstance(spec, SubWorkflow): if isinstance(spec, SubWorkflow):
br_state = self.serialize_sub_workflow(spec) br_state = self.serialize_sub_workflow(spec)
s_state['file'] = br_state['file'] s_state['file'] = br_state['file']
s_state['in_assign'] = br_state['in_assign'] s_state['in_assign'] = br_state['in_assign']
s_state['out_assign'] = br_state['out_assign'] s_state['out_assign'] = br_state['out_assign']
s_state['times'] = self.serialize_arg(spec.times) s_state['times'] = self.serialize_arg(spec.times)
s_state['prevtaskclass'] = spec.prevtaskclass
return s_state return s_state
def deserialize_multi_instance(self, wf_spec, s_state, cls=None): def deserialize_multi_instance(self, wf_spec, s_state):
if cls == None: spec = MultiInstance(wf_spec, s_state['name'], times=self.deserialize_arg(s_state['times']))
cls = MultiInstance(wf_spec, if isinstance(spec, SubWorkflow):
s_state['name'],
times=self.deserialize_arg(s_state['times']))
if isinstance(s_state['times'],list):
s_state['times'] = self.deserialize_arg(s_state['times'])
cls.times = s_state['times']
if isinstance(cls, SubWorkflow):
if s_state.get('file'): if s_state.get('file'):
cls.file = self.deserialize_arg(s_state['file']) spec.file = self.deserialize_arg(s_state['file'])
else: else:
cls.file = None spec.file = None
cls.in_assign = self.deserialize_list(s_state['in_assign']) spec.in_assign = self.deserialize_list(s_state['in_assign'])
cls.out_assign = self.deserialize_list(s_state['out_assign']) spec.out_assign = self.deserialize_list(s_state['out_assign'])
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
self.deserialize_task_spec(wf_spec, s_state, spec=cls) return spec
return cls
def serialize_release_mutex(self, spec): def serialize_release_mutex(self, spec):
s_state = self.serialize_task_spec(spec) s_state = self.serialize_task_spec(spec)
@ -398,7 +343,6 @@ class DictionarySerializer(Serializer):
self.deserialize_task_spec(wf_spec, s_state, spec=spec) self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec return spec
def deserialize_generic(self, wf_spec, s_state,newclass): def deserialize_generic(self, wf_spec, s_state,newclass):
assert isinstance(wf_spec, WorkflowSpec) assert isinstance(wf_spec, WorkflowSpec)
spec = newclass(wf_spec, s_state['name']) spec = newclass(wf_spec, s_state['name'])
@ -486,71 +430,33 @@ class DictionarySerializer(Serializer):
return spec return spec
def serialize_workflow_spec(self, spec, **kwargs): def serialize_workflow_spec(self, spec, **kwargs):
s_state = dict(name=spec.name, s_state = dict(name=spec.name, description=spec.description, file=spec.file)
description=spec.description, s_state['task_specs'] = dict(
file=spec.file) (k, v.serialize(self))
for k, v in list(spec.task_specs.items())
if 'Root' not in spec.task_specs: )
# This is to fix up the case when we
# load in a task spec and there is no root object.
# it causes problems when we deserialize and then re-serialize
# because the deserialize process adds a root.
root = Simple(spec, 'Root')
spec.task_specs['Root'] = root
mylist = [(k, v.serialize(self)) for k, v in list(spec.task_specs.items())]
# As we serialize back up, keep only one copy of any sub_workflow
s_state['sub_workflows'] = {}
for name, task in mylist:
if 'spec' in task:
spec = json.loads(task['spec'])
if 'sub_workflows' in spec:
s_state['sub_workflows'].update(spec['sub_workflows'])
del spec['sub_workflows']
if spec['name'] not in s_state['sub_workflows']:
s_state['sub_workflows'][spec['name']] = json.dumps(spec)
task['spec_name'] = spec['name']
del task['spec']
if hasattr(spec,'end'):
s_state['end']=spec.end.id
s_state['task_specs'] = dict(mylist)
return s_state return s_state
def _deserialize_workflow_spec_task_spec(self, spec, task_spec, name): def _deserialize_workflow_spec_task_spec(self, spec, task_spec, name):
task_spec.inputs = [spec.get_task_spec_from_id(t) for t in task_spec.inputs] task_spec.inputs = [spec.get_task_spec_from_name(t) for t in task_spec.inputs]
task_spec.outputs = [spec.get_task_spec_from_id(t) for t in task_spec.outputs] task_spec.outputs = [spec.get_task_spec_from_name(t) for t in task_spec.outputs]
def _prevtaskclass_bases(self, oldtask):
return (oldtask)
def deserialize_workflow_spec(self, s_state, **kwargs): def deserialize_workflow_spec(self, s_state, **kwargs):
spec = WorkflowSpec(s_state['name'], filename=s_state['file']) spec = WorkflowSpec(s_state['name'], filename=s_state['file'])
spec.description = s_state['description'] spec.description = s_state['description']
# Handle Start Task # Handle Start Task
spec.start = None spec.start = None
# Store all sub-workflows so they can be referenced.
if 'sub_workflows' in s_state:
# Hate the whole json dumps thing, why do we do this?
self.SPEC_STATES.update(s_state['sub_workflows'])
del spec.task_specs['Start'] del spec.task_specs['Start']
start_task_spec_state = s_state['task_specs']['Start'] start_task_spec_state = s_state['task_specs']['Start']
start_task_spec = StartTask.deserialize(self, spec, start_task_spec_state) start_task_spec = StartTask.deserialize(self, spec, start_task_spec_state)
spec.start = start_task_spec spec.start = start_task_spec
spec.task_specs['Start'] = start_task_spec spec.task_specs['Start'] = start_task_spec
for name, task_spec_state in list(s_state['task_specs'].items()): for name, task_spec_state in list(s_state['task_specs'].items()):
if name == 'Start': if name == 'Start':
continue continue
prevtask = task_spec_state.get('prevtaskclass', None) task_spec_cls = get_class(task_spec_state['class'])
if prevtask:
oldtask = get_class(prevtask)
task_spec_cls = type(task_spec_state['class'],
self._prevtaskclass_bases(oldtask), {})
else:
task_spec_cls = get_class(task_spec_state['class'])
task_spec = task_spec_cls.deserialize(self, spec, task_spec_state) task_spec = task_spec_cls.deserialize(self, spec, task_spec_state)
spec.task_specs[name] = task_spec spec.task_specs[name] = task_spec
@ -558,7 +464,7 @@ class DictionarySerializer(Serializer):
self._deserialize_workflow_spec_task_spec(spec, task_spec, name) self._deserialize_workflow_spec_task_spec(spec, task_spec, name)
if s_state.get('end', None): if s_state.get('end', None):
spec.end = spec.get_task_spec_from_id(s_state['end']) spec.end = spec.get_task_spec_from_name(s_state['end'])
assert spec.start is spec.get_task_spec_from_name('Start') assert spec.start is spec.get_task_spec_from_name('Start')
return spec return spec
@ -578,25 +484,19 @@ class DictionarySerializer(Serializer):
return s_state return s_state
def deserialize_workflow(self, s_state, wf_class=Workflow, wf_spec=None, **kwargs): def deserialize_workflow(self, s_state, wf_class=Workflow, **kwargs):
"""It is possible to override the workflow class, and specify a """It is possible to override the workflow class, and specify a
workflow_spec, otherwise the spec is assumed to be serialized in the workflow_spec, otherwise the spec is assumed to be serialized in the
s_state['wf_spec']""" s_state['wf_spec']"""
if wf_spec is None: if isinstance(s_state['wf_spec'], str):
# The json serializer serializes the spec as a string and then serializes it again, hence this check spec_dct = json.loads(s_state['wf_spec'])
# I'm not confident that this is going to actually work, but this serializer is so fundamentally flawed
# that I'm not going to put the effort in to be sure this works.
if isinstance(s_state['wf_spec'], str):
spec_dct = json.loads(s_state['wf_spec'])
else:
spec_dct = s_state['wf_spec']
reset_specs = [spec['name'] for spec in spec_dct['task_specs'].values() if spec['class'].endswith('LoopResetTask')]
for name in reset_specs:
s_state['wf_spec']['task_specs'].pop(name)
wf_spec = self.deserialize_workflow_spec(s_state['wf_spec'], **kwargs)
else: else:
reset_specs = [] spec_dct = s_state['wf_spec']
reset_specs = [spec['name'] for spec in spec_dct['task_specs'].values() if spec['class'].endswith('LoopResetTask')]
for name in reset_specs:
s_state['wf_spec']['task_specs'].pop(name)
wf_spec = self.deserialize_workflow_spec(s_state['wf_spec'], **kwargs)
workflow = wf_class(wf_spec) workflow = wf_class(wf_spec)
workflow.data = self.deserialize_dict(s_state['data']) workflow.data = self.deserialize_dict(s_state['data'])
@ -623,35 +523,24 @@ class DictionarySerializer(Serializer):
return workflow return workflow
def serialize_task(self, task, skip_children=False, allow_subs=False): def serialize_task(self, task, skip_children=False):
"""
:param allow_subs: Allows sub-serialization to take place, otherwise
assumes that the subworkflow is stored in internal data and raises an error.
"""
assert isinstance(task, Task) assert isinstance(task, Task)
if isinstance(task.task_spec, SubWorkflow):
# Please note, the BPMN Serializer DOES allow sub-workflows. This is
# for backwards compatibility and support of the original parsers.
if not allow_subs and isinstance(task.task_spec, SubWorkflow):
raise TaskNotSupportedError( raise TaskNotSupportedError(
"Subworkflow tasks cannot be serialized (due to their use of" + "Subworkflow tasks cannot be serialized (due to their use of" +
" internal_data to store the subworkflow).") " internal_data to store the subworkflow).")
s_state = dict() s_state = dict()
s_state['id'] = task.id s_state['id'] = task.id
s_state['workflow_name'] = task.workflow.name s_state['workflow_name'] = task.workflow.name
s_state['parent'] = task.parent.id if task.parent is not None else None s_state['parent'] = task.parent.id if task.parent is not None else None
if not skip_children: if not skip_children:
s_state['children'] = [ s_state['children'] = [self.serialize_task(child) for child in task.children]
self.serialize_task(child) for child in task.children]
s_state['state'] = task.state s_state['state'] = task.state
s_state['triggered'] = task.triggered s_state['triggered'] = task.triggered
s_state['task_spec'] = task.task_spec.name s_state['task_spec'] = task.task_spec.name
s_state['last_state_change'] = task.last_state_change s_state['last_state_change'] = task.last_state_change
s_state['data'] = self.serialize_dict(task.data) s_state['data'] = self.serialize_dict(task.data)
s_state['internal_data'] = task.internal_data s_state['internal_data'] = task.internal_data
return s_state return s_state
def deserialize_task(self, workflow, s_state, ignored_specs=None): def deserialize_task(self, workflow, s_state, ignored_specs=None):
@ -664,9 +553,6 @@ class DictionarySerializer(Serializer):
raise MissingSpecError("Unknown task spec: " + old_spec_name) raise MissingSpecError("Unknown task spec: " + old_spec_name)
task = Task(workflow, task_spec) task = Task(workflow, task_spec)
if getattr(task_spec,'isSequential',False) and s_state['internal_data'].get('splits') is not None:
task.task_spec.expanded = s_state['internal_data']['splits']
task.id = s_state['id'] task.id = s_state['id']
# as the task_tree might not be complete yet # as the task_tree might not be complete yet
# keep the ids so they can be processed at the end # keep the ids so they can be processed at the end

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # This file is part of SpiffWorkflow.
#
# This library is free software; you can redistribute it and/or # SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.

View File

@ -1,3 +1,20 @@
# This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# SpiffWorkflow 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
class TaskSpecNotSupportedError(ValueError): class TaskSpecNotSupportedError(ValueError):
pass pass

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # This file is part of SpiffWorkflow.
#
# This library is free software; you can redistribute it and/or # SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -14,6 +14,7 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import json import json
import uuid import uuid
from ..operators import Attrib from ..operators import Attrib

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # Copyright (C) 2007-2012 Samuel Abels, 2023 Sartography
# Copyright (C) 2007-2012 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This file is part of SpiffWorkflow.
#
# SpiffWorkflow is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -16,6 +16,7 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .. import operators from .. import operators
from ..specs.Simple import Simple from ..specs.Simple import Simple
from ..specs.WorkflowSpec import WorkflowSpec from ..specs.WorkflowSpec import WorkflowSpec

View File

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*- # This file is part of SpiffWorkflow.
#
from builtins import str # SpiffWorkflow is free software; you can redistribute it and/or
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public # modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either # License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version. # version 3.0 of the License, or (at your option) any later version.
# #
# This library is distributed in the hope that it will be useful, # SpiffWorkflow is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # Lesser General Public License for more details.
@ -15,6 +14,7 @@ from builtins import str
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import warnings import warnings
from lxml import etree from lxml import etree
from lxml.etree import SubElement from lxml.etree import SubElement
@ -24,7 +24,6 @@ from ..operators import (Attrib, Assign, PathAttrib, Equal, NotEqual, GreaterTha
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute from ..specs.Execute import Execute
@ -287,15 +286,11 @@ class XmlSerializer(Serializer):
""" """
Serializes common attributes of :meth:`SpiffWorkflow.specs.TaskSpec`. Serializes common attributes of :meth:`SpiffWorkflow.specs.TaskSpec`.
""" """
if spec.id is not None:
SubElement(elem, 'id').text = str(spec.id)
SubElement(elem, 'name').text = spec.name SubElement(elem, 'name').text = spec.name
if spec.description: if spec.description:
SubElement(elem, 'description').text = spec.description SubElement(elem, 'description').text = spec.description
if spec.manual: if spec.manual:
SubElement(elem, 'manual') SubElement(elem, 'manual')
if spec.internal:
SubElement(elem, 'internal')
SubElement(elem, 'lookahead').text = str(spec.lookahead) SubElement(elem, 'lookahead').text = str(spec.lookahead)
inputs = [t.name for t in spec.inputs] inputs = [t.name for t in spec.inputs]
outputs = [t.name for t in spec.outputs] outputs = [t.name for t in spec.outputs]
@ -316,11 +311,8 @@ class XmlSerializer(Serializer):
def deserialize_task_spec(self, wf_spec, elem, spec_cls, **kwargs): def deserialize_task_spec(self, wf_spec, elem, spec_cls, **kwargs):
name = elem.findtext('name') name = elem.findtext('name')
spec = spec_cls(wf_spec, name, **kwargs) spec = spec_cls(wf_spec, name, **kwargs)
theid = elem.findtext('id')
spec.id = theid if theid is not None else None
spec.description = elem.findtext('description', spec.description) spec.description = elem.findtext('description', spec.description)
spec.manual = elem.findtext('manual', spec.manual) spec.manual = elem.findtext('manual', spec.manual)
spec.internal = elem.find('internal') is not None
spec.lookahead = int(elem.findtext('lookahead', spec.lookahead)) spec.lookahead = int(elem.findtext('lookahead', spec.lookahead))
data_elem = elem.find('data') data_elem = elem.find('data')
@ -384,37 +376,6 @@ class XmlSerializer(Serializer):
def deserialize_cancel_task(self, wf_spec, elem, cls=CancelTask, **kwargs): def deserialize_cancel_task(self, wf_spec, elem, cls=CancelTask, **kwargs):
return self.deserialize_trigger(wf_spec, elem, cls, **kwargs) return self.deserialize_trigger(wf_spec, elem, cls, **kwargs)
def serialize_celery(self, spec, elem=None):
if elem is None:
elem = etree.Element('celery')
SubElement(elem, 'call').text = spec.call
args_elem = SubElement(elem, 'args')
self.serialize_value_list(args_elem, spec.args)
kwargs_elem = SubElement(elem, 'kwargs')
self.serialize_value_map(kwargs_elem, spec.kwargs)
if spec.merge_results:
SubElement(elem, 'merge-results')
SubElement(elem, 'result-key').text = spec.result_key
return self.serialize_task_spec(spec, elem)
def deserialize_celery(self, wf_spec, elem, cls=Celery, **kwargs):
call = elem.findtext('call')
args = self.deserialize_value_list(elem.find('args'))
result_key = elem.findtext('call')
merge_results = elem.find('merge-results') is not None
spec = self.deserialize_task_spec(wf_spec,
elem,
cls,
call=call,
call_args=args,
result_key=result_key,
merge_results=merge_results,
**kwargs)
spec.kwargs = self.deserialize_value_map(elem.find('kwargs'))
return spec
def serialize_choose(self, spec, elem=None): def serialize_choose(self, spec, elem=None):
if elem is None: if elem is None:
elem = etree.Element('choose') elem = etree.Element('choose')
@ -526,7 +487,7 @@ class XmlSerializer(Serializer):
def deserialize_multi_instance(self, wf_spec, elem, cls=None, def deserialize_multi_instance(self, wf_spec, elem, cls=None,
**kwargs): **kwargs):
if cls == None: if cls is None:
cls = MultiInstance cls = MultiInstance
#cls = MultiInstance(wf_spec,elem.find('name'),elem.find('times')) #cls = MultiInstance(wf_spec,elem.find('name'),elem.find('times'))
times = self.deserialize_value(elem.find('times')) times = self.deserialize_value(elem.find('times'))

Some files were not shown because too many files have changed in this diff Show More