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
is managed through GitHub's actions. The configuration of which can be
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](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)
[![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)
[![SpiffWorkflow](https://github.com/sartography/SpiffWorkflow/actions/workflows/tests.yaml/badge.svg)](https://github.com/sartography/SpiffWorkflow/actions/workflows/tests.yaml)
[![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)
[![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 -*-
import re
import datetime
import operator
from datetime import timedelta
from decimal import Decimal
from .PythonScriptEngine import PythonScriptEngine
# Copyright (C) 2020 Kelly McDonald
# Copyright (C) 2020 Kelly McDonald, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -24,6 +17,13 @@ from .PythonScriptEngine import PythonScriptEngine
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 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):
return datetime.datetime.strptime(datestr,parsestr)
@ -79,8 +79,8 @@ class FeelNot():
def feelConcatenate(*lst):
ilist = []
for l in lst:
ilist = ilist + l
for list_item in lst:
ilist = ilist + list_item
return ilist
def feelAppend(lst,item):
@ -144,7 +144,7 @@ def feelFilter(var,a,b,op,column=None):
newvar.append({'key':key,'value':var[key]})
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)]
else:
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:
external_methods = {}
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 -*-
import ast
import sys
import traceback
import warnings
from .PythonScriptEngineEnvironment import TaskDataEnvironment
from ..exceptions import SpiffWorkflowException, WorkflowTaskException
# Copyright (C) 2020 Kelly McDonald
# Copyright (C) 2020 Kelly McDonald, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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
# 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):
"""
@ -40,8 +41,8 @@ class PythonScriptEngine(object):
def __init__(self, default_globals=None, scripting_additions=None, environment=None):
if default_globals is not None or scripting_additions is not None:
warnings.warn(f'default_globals and scripting_additions are deprecated. '
f'Please provide an environment such as TaskDataEnvrionment',
warnings.warn('default_globals and scripting_additions are deprecated. '
'Please provide an environment such as TaskDataEnvrionment',
DeprecationWarning, stacklevel=2)
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 warnings
@ -96,7 +115,7 @@ class Box(dict):
def __getattr__(self, attr):
try:
output = self[attr]
except:
except Exception:
raise AttributeError(
"Dictionary has no attribute '%s' " % str(attr))
return output

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.

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):

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -23,26 +23,36 @@ import os
from lxml import etree
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 ..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 .node_parser import DEFAULT_NSMAP
from .spec_description import SPEC_DESCRIPTIONS
from .util import full_tag, xpath_eval, first
from .TaskParser import TaskParser
from .task_parsers import (
@ -86,7 +96,7 @@ class BpmnValidator:
except ValidationException as ve:
ve.file_name = filename
ve.line_number = self.validator.error_log.last_error.line
except LxmlError as le:
except LxmlError:
last_error = self.validator.error_log.last_error
raise ValidationException(last_error.message, file_name=filename,
line_number=last_error.line)
@ -132,14 +142,14 @@ class BpmnParser(object):
DATA_STORE_CLASSES = {}
def __init__(self, namespaces=None, validator=None):
def __init__(self, namespaces=None, validator=None, spec_descriptions=SPEC_DESCRIPTIONS):
"""
Constructor.
"""
self.namespaces = namespaces or DEFAULT_NSMAP
self.validator = validator
self.spec_descriptions = spec_descriptions
self.process_parsers = {}
self.process_parsers_by_name = {}
self.collaborations = {}
self.process_dependencies = set()
self.messages = {}
@ -153,15 +163,13 @@ class BpmnParser(object):
return self.PARSER_CLASSES[tag]
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
by name first.
"""
if process_id_or_name in self.process_parsers_by_name:
return self.process_parsers_by_name[process_id_or_name]
elif process_id_or_name in self.process_parsers:
return self.process_parsers[process_id_or_name]
if process_id in self.process_parsers:
return self.process_parsers[process_id]
def get_process_ids(self):
"""Returns a list of process IDs"""
@ -186,7 +194,19 @@ class BpmnParser(object):
"""
for filename in filenames:
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):
"""
@ -286,36 +306,36 @@ class BpmnParser(object):
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)
if parser.get_id() in self.process_parsers:
raise ValidationException(f'Duplicate process ID: {parser.get_id()}', node=node, file_name=filename)
if parser.get_name() in self.process_parsers_by_name:
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
if parser.bpmn_id in self.process_parsers:
raise ValidationException(f'Duplicate process ID: {parser.bpmn_id}', node=node, file_name=filename)
self.process_parsers[parser.bpmn_id] = parser
def get_process_dependencies(self):
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
instance of BpmnProcessSpec (i.e. WorkflowSpec)
for the given process ID or name. The Name is matched first.
"""
parser = self.get_process_parser(process_id_or_name)
if parser is None:
parser = self.get_process_parser(process_id)
if required and parser is None:
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"{', '.join(self.get_process_ids())}?")
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 {}
wf_spec = self.get_spec(name)
for task_spec in wf_spec.task_specs.values():
if isinstance(task_spec, SubWorkflowTask) and task_spec.spec not in used:
used[task_spec.spec] = self.get_spec(task_spec.spec)
if isinstance(task_spec, SubWorkflowTaskMixin) and task_spec.spec not in used:
subprocess_spec = self.get_spec(task_spec.spec, required=require_call_activity_specs)
used[task_spec.spec] = subprocess_spec
if subprocess_spec is not None:
self.get_subprocess_specs(task_spec.spec, used)
return used
@ -332,16 +352,24 @@ class BpmnParser(object):
self.find_all_specs()
spec = BpmnProcessSpec(name)
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)
end = EndEvent(spec, 'End Collaboration', NoneEventDefinition())
end = end_type(spec, 'End Collaboration', NoneEventDefinition())
end.connect(spec.end)
for process in self.collaborations[name]:
process_parser = self.get_process_parser(process)
if process_parser and process_parser.process_executable:
participant = CallActivity(spec, process, process)
sp_spec = self.get_spec(process)
subprocesses[process] = sp_spec
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)
subprocesses[process] = self.get_spec(process)
subprocesses.update(self.get_subprocess_specs(process))
return spec, subprocesses

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -18,7 +18,7 @@
# 02110-1301 USA
from .ValidationException import ValidationException
from ..specs.BpmnProcessSpec import BpmnProcessSpec
from ..specs.bpmn_process_spec import BpmnProcessSpec
from ..specs.data_spec import DataObject
from .node_parser import NodeParser
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)
"""
return self.node.get('name', default=self.get_id())
return self.node.get('name', default=self.bpmn_id)
def has_lanes(self) -> bool:
"""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')
if not start_node_list and self.process_executable:
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)
# Get the data objects
for obj in self.xpath('./bpmn:dataObject'):
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.
io_spec = first(self.xpath('./bpmn:ioSpecification'))

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,17 +17,21 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .ValidationException import ValidationException
from ..specs.events.IntermediateEvent import _BoundaryEventParent
from ..specs.events.event_definitions import CancelEventDefinition
from ..specs.MultiInstanceTask import StandardLoopTask, SequentialMultiInstanceTask, ParallelMultiInstanceTask
from ..specs.SubWorkflowTask import TransactionSubprocess
from ..specs.ExclusiveGateway import ExclusiveGateway
from ..specs.InclusiveGateway import InclusiveGateway
from ..specs.data_spec import TaskDataReference
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import TransactionSubprocess
from SpiffWorkflow.bpmn.specs.mixins.exclusive_gateway import ExclusiveGateway
from SpiffWorkflow.bpmn.specs.mixins.inclusive_gateway import InclusiveGateway
from SpiffWorkflow.bpmn.specs.defaults import (
StandardLoopTask,
SequentialMultiInstanceTask,
ParallelMultiInstanceTask
)
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 .node_parser import NodeParser
from .ValidationException import ValidationException
class TaskParser(NodeParser):
@ -100,9 +104,11 @@ class TaskParser(NodeParser):
cardinality = self.xpath(f'./{prefix}/bpmn:loopCardinality')
loop_input = self.xpath(f'./{prefix}/bpmn:loopDataInputRef')
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:
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
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:
try:
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')
else:
loop_input = TaskDataReference(loop_input)
@ -125,7 +131,7 @@ class TaskParser(NodeParser):
try:
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]
except:
except Exception:
self.raise_validation_exception('The loop output data reference is missing from the IO specification')
else:
loop_output = TaskDataReference(loop_output)
@ -155,7 +161,7 @@ class TaskParser(NodeParser):
def _add_boundary_event(self, children):
parent = _BoundaryEventParent(
self.spec, '%s.BoundaryEventParent' % self.get_id(),
self.spec, '%s.BoundaryEventParent' % self.bpmn_id,
self.task, lane=self.task.lane)
self.process_parser.parsed_nodes[self.node.get('id')] = parent
parent.connect(self.task)
@ -176,10 +182,6 @@ class TaskParser(NodeParser):
# 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!
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')
if len(io_spec) > 0:
@ -193,30 +195,32 @@ class TaskParser(NodeParser):
if len(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:
parent = self._add_boundary_event(boundary_event_nodes)
else:
self.process_parser.parsed_nodes[self.node.get('id')] = self.task
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():
self.raise_validation_exception('Multiple outgoing flows are not supported for tasks of type')
for sequence_flow in outgoing:
target_ref = sequence_flow.get('targetRef')
try:
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, '
'perhaps a form has the same ID? (%s)' % target_ref)
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))
if children:
# 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"]))
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)
"""
return target_ref or self.get_id()
return target_ref or self.bpmn_id
def create_task(self):
"""
Create an instance of the task appropriately. A subclass can override
this method to get extra information from the node.
"""
return self.spec_class(self.spec, self.get_task_spec_name(),
lane=self.lane,
description=self.node.get('name', None),
position=self.position)
return self.spec_class(self.spec, self.bpmn_id, **self.bpmn_attributes)
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
#
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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 .TaskParser import TaskParser
from .util import first, one
from ..specs.events.event_definitions import (
from ..specs.event_definitions import (
MultipleEventDefinition,
TimeDateEventDefinition,
DurationTimerEventDefinition,
@ -16,7 +33,8 @@ from ..specs.events.event_definitions import (
SignalEventDefinition,
CancelEventDefinition,
TerminateEventDefinition,
NoneEventDefinition
NoneEventDefinition,
CorrelationProperty
)
CANCEL_EVENT_XPATH = './/bpmn:cancelEventDefinition'
@ -31,8 +49,26 @@ TIMER_EVENT_XPATH = './/bpmn:timerEventDefinition'
class EventDefinitionParser(TaskParser):
"""This class provvides methods for parsing different event definitions."""
def parse_cancel_event(self):
return CancelEventDefinition()
def __init__(self, process_parser, spec_class, node, nsmap=None, lane=None):
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):
"""Parse the errorEventDefinition node and return an instance of ErrorEventDefinition."""
@ -43,7 +79,7 @@ class EventDefinitionParser(TaskParser):
name = error.get('name')
else:
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):
"""Parse the escalationEventDefinition node and return an instance of EscalationEventDefinition."""
@ -55,7 +91,7 @@ class EventDefinitionParser(TaskParser):
name = escalation.get('name')
else:
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):
@ -63,11 +99,13 @@ class EventDefinitionParser(TaskParser):
if message_ref is not None:
message = one(self.doc_xpath('.//bpmn:message[@id="%s"]' % message_ref))
name = message.get('name')
description = self.get_event_description(message_event)
correlations = self.get_message_correlations(message_ref)
else:
name = message_event.getparent().get('name')
description = 'Message'
correlations = {}
return MessageEventDefinition(name, correlations)
return MessageEventDefinition(name, correlations, description=description)
def parse_signal_event(self, signal_event):
"""Parse the signalEventDefinition node and return an instance of SignalEventDefinition."""
@ -78,26 +116,26 @@ class EventDefinitionParser(TaskParser):
name = signal.get('name')
else:
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."""
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."""
try:
description = self.get_event_description(event)
name = self.node.get('name', self.node.get('id'))
time_date = first(self.xpath('.//bpmn:timeDate'))
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'))
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'))
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)
except Exception as e:
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]:
self.spec.correlation_keys[key].append(prop.name)
kwargs = {
'lane': self.lane,
'description': self.node.get('name', None),
'position': self.position,
}
kwargs = self.bpmn_attributes
if cancel_activity is not None:
kwargs['cancel_activity'] = cancel_activity
interrupt = 'Interrupting' if cancel_activity else 'Non-Interrupting'
kwargs['description'] = interrupt + ' ' + kwargs['description']
if parallel is not None:
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):
"""Returns all event definitions it can find in given list of xpaths"""
@ -142,29 +178,31 @@ class EventDefinitionParser(TaskParser):
event_definitions = []
for path in xpaths:
for event in self.xpath(path):
if event is not None:
self.event_nodes.append(event)
if path == MESSAGE_EVENT_XPATH:
event_definitions.append(self.parse_message_event(event))
elif path == SIGNAL_EVENT_XPATH:
event_definitions.append(self.parse_signal_event(event))
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:
event_definitions.append(self.parse_cancel_event())
event_definitions.append(self.parse_cancel_event(event))
elif path == ERROR_EVENT_XPATH:
event_definitions.append(self.parse_error_event(event))
elif path == ESCALATION_EVENT_XPATH:
event_definitions.append(self.parse_escalation_event(event))
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'
if len(event_definitions) == 0:
return NoneEventDefinition()
return NoneEventDefinition(description='Default')
elif len(event_definitions) == 1:
return event_definitions[0]
else:
return MultipleEventDefinition(event_definitions, parallel)
return MultipleEventDefinition(event_definitions, parallel, description='Multiple')
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.specs.data_spec import TaskDataReference, BpmnIoSpecification
from .util import first
@ -17,11 +36,25 @@ class NodeParser:
self.nsmap = nsmap or DEFAULT_NSMAP
self.filename = filename
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')
@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):
return self._xpath(self.node, xpath, extra_ns)
@ -76,10 +109,10 @@ class NodeParser:
data_refs = {}
for elem in self.xpath('./bpmn:ioSpecification/bpmn:dataInput'):
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'):
ref = self.create_data_spec(elem, TaskDataReference)
data_refs[ref.name] = ref
data_refs[ref.bpmn_id] = ref
inputs, outputs = [], []
for ref in self.xpath('./bpmn:ioSpecification/bpmn:inputSet/bpmn:dataInputRefs'):
@ -96,15 +129,19 @@ class NodeParser:
def parse_extensions(self, node=None):
return {}
def _get_lane(self):
noderef = first(self.doc_xpath(f".//bpmn:flowNodeRef[text()='{self.get_id()}']"))
if noderef is not None:
return noderef.getparent().get('name')
def _get_position(self):
bounds = first(self.doc_xpath(f".//bpmndi:BPMNShape[@bpmnElement='{self.get_id()}']//dc:Bounds"))
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):
noderef = first(self.doc_xpath(f".//bpmn:flowNodeRef[text()='{self.bpmn_id}']"))
if noderef is not None:
return noderef.getparent().get('name')
def _xpath(self, node, xpath, extra_ns=None):
if extra_ns is not None:

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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -85,13 +86,6 @@ class SubprocessParser:
'No "calledElement" attribute for Call Activity.',
node=task_parser.node,
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
@ -99,10 +93,7 @@ class SubWorkflowParser(TaskParser):
def create_task(self):
subworkflow_spec = SubprocessParser.get_subprocess_spec(self)
return self.spec_class(
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
class CallActivityParser(TaskParser):
@ -110,23 +101,14 @@ class CallActivityParser(TaskParser):
def create_task(self):
subworkflow_spec = SubprocessParser.get_call_activity_spec(self)
return self.spec_class(
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
class ScriptTaskParser(TaskParser):
"""
Parses a script task
"""
"""Parses a script task"""
def create_task(self):
script = self.get_script()
return self.spec_class(self.spec, self.get_task_spec_name(), script,
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, script=self.get_script(), **self.bpmn_attributes)
def get_script(self):
"""
@ -138,6 +120,6 @@ class ScriptTaskParser(TaskParser):
return one(self.xpath('.//bpmn:script')).text
except AssertionError as ae:
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)

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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 .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,
ErrorEventDefinition,
EscalationEventDefinition,
@ -13,6 +30,7 @@ from ..specs.events.event_definitions import (
CycleTimerEventDefinition,
MultipleEventDefinition,
)
from .helpers.spec import EventDefinitionConverter
class CancelEventDefinitionConverter(EventDefinitionConverter):
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
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 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 ...specs.BpmnSpecMixin import BpmnSpecMixin
from ...specs.events.event_definitions import NamedEventDefinition, TimerEventDefinition
from ...specs.events.event_definitions import CorrelationProperty
from ....operators import Attrib, PathAttrib
from SpiffWorkflow.operators import Attrib, PathAttrib
from SpiffWorkflow.bpmn.specs.mixins.bpmn_spec_mixin import BpmnSpecMixin
from SpiffWorkflow.bpmn.specs.event_definitions import (
NamedEventDefinition,
TimerEventDefinition,
CorrelationProperty
)
class BpmnSpecConverter:
@ -57,7 +80,7 @@ class BpmnDataSpecificationConverter(BpmnSpecConverter):
"""
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):
return self.spec_class(**dct)
@ -72,7 +95,11 @@ class EventDefinitionConverter(BpmnSpecConverter):
"""
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)):
dct['name'] = event_definition.name
return dct
@ -105,7 +132,7 @@ class TaskSpecConverter(BpmnSpecConverter):
modules of this package; the `camunda`,`dmn`, and `spiff` serialization packages contain other
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.
:param spec: the task spec to be converted
@ -113,38 +140,17 @@ class TaskSpecConverter(BpmnSpecConverter):
Returns:
a dictionary of standard task spec attributes
"""
dct = {
'id': spec.id,
return {
'name': spec.name,
'description': spec.description,
'manual': spec.manual,
'internal': spec.internal,
'lookahead': spec.lookahead,
'inputs': [task.name for task in spec.inputs],
'outputs': [task.name for task in spec.outputs],
}
# This stuff is also all defined in the base task spec, but can contain data, so we need
# 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 {
'bpmn_id': spec.bpmn_id,
'bpmn_name': spec.bpmn_name,
'lane': spec.lane,
'documentation': spec.documentation,
'position': spec.position,
'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 ],
'io_specification': self.registry.convert(spec.io_specification),
@ -189,38 +195,36 @@ class TaskSpecConverter(BpmnSpecConverter):
'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
task spec attributes as well as attributes added by `BpmnSpecMixin`.
:param dct: the dictionary to create the task spec from
:param include_data: whether or not to include task spec data attributes
Returns:
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')
outputs = dct.pop('outputs')
spec = self.spec_class(**dct)
spec.internal = internal
wf_spec = dct.pop('wf_spec')
name = dct.pop('name')
bpmn_id = dct.pop('bpmn_id')
spec = self.spec_class(wf_spec, name, **dct)
spec.inputs = inputs
spec.outputs = outputs
spec.id = dct['id']
if include_data:
spec.data = self.registry.restore(dct.get('data', {}))
spec.defines = self.registry.restore(dct.get('defines', {}))
spec.pre_assign = self.registry.restore(dct.get('pre_assign', {}))
spec.post_assign = self.registry.restore(dct.get('post_assign', {}))
if issubclass(self.spec_class, BpmnSpecMixin) and bpmn_id != name:
# This is a hack for multiinstance tasks :( At least it is simple.
# Ideally I'd fix it in the parser, but I'm afraid of quickly running into a wall there
spec.bpmn_id = bpmn_id
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))
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
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):
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 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
@ -29,7 +48,7 @@ def convert_timer_expressions(dct):
elif isinstance(dt, timedelta):
spec['event_definition']['expression'] = f"'{td_to_iso(dt)}'"
spec['event_definition']['typename'] = 'DurationTimerEventDefinition'
except:
except Exception:
raise VersionMigrationError(message.format(spec=spec['name']))
def convert_cycle(spec, task):
@ -47,7 +66,7 @@ def convert_timer_expressions(dct):
'next': datetime.combine(dt.date(), dt.time(), LOCALTZ).isoformat(),
'duration': duration.total_seconds(),
}
except:
except Exception:
raise VersionMigrationError(message.format(spec=spec['name']))
if spec['typename'] == 'StartEvent':
@ -65,7 +84,8 @@ def convert_timer_expressions(dct):
task['children'].remove(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) ]:
spec['event_definition']['name'] = spec['event_definition'].pop('label')
if spec['event_definition']['typename'] == 'TimerEventDefinition':
@ -143,3 +163,97 @@ def update_task_states(dct):
update(dct)
for sp in dct['subprocesses'].values():
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 .version_1_1 import move_subprocesses_to_top
@ -8,6 +27,8 @@ from .version_1_2 import (
check_multiinstance,
remove_loop_reset,
update_task_states,
convert_simple_tasks,
update_bpmn_attributes,
)
def from_version_1_1(old):
@ -38,6 +59,8 @@ def from_version_1_1(old):
check_multiinstance(new)
remove_loop_reset(new)
update_task_states(new)
convert_simple_tasks(new)
update_bpmn_attributes(new)
new['VERSION'] = "1.2"
return new

View File

@ -1,5 +1,24 @@
from ..specs.BpmnProcessSpec import BpmnProcessSpec
from ..specs.events.IntermediateEvent import _BoundaryEventParent
# 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.bpmn_process_spec import BpmnProcessSpec
from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
from .helpers.spec import WorkflowSpecConverter
@ -39,12 +58,9 @@ class BpmnProcessSpecConverter(WorkflowSpecConverter):
def from_dict(self, dct):
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
# the BPMN process spec doesn't pass it in, so we have to delete the auto generated Start task.
# These are automatically created with a workflow and should be replaced
del spec.task_specs['Start']
spec.start = None
# These are also automatically created with a workflow and should be replaced
del spec.task_specs['End']
del spec.task_specs[f'{spec.name}.EndJoin']
@ -60,6 +76,7 @@ class BpmnProcessSpecConverter(WorkflowSpecConverter):
# Add messaging related stuff
spec.correlation_keys = dct.pop('correlation_keys', {})
dct['task_specs'].pop('Root', None)
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.
# 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 ...specs.Simple import Simple
from ..specs.BpmnProcessSpec import _EndJoin
from ..specs.BpmnSpecMixin import _BpmnCondition
from ..specs.NoneTask import NoneTask
from ..specs.UserTask import UserTask
from ..specs.ManualTask import ManualTask
from ..specs.ScriptTask import ScriptTask
from ..specs.MultiInstanceTask import StandardLoopTask, SequentialMultiInstanceTask, ParallelMultiInstanceTask
from ..specs.SubWorkflowTask import CallActivity, TransactionSubprocess, SubWorkflowTask
from ..specs.ExclusiveGateway import ExclusiveGateway
from ..specs.InclusiveGateway import InclusiveGateway
from ..specs.ParallelGateway import ParallelGateway
from ..specs.events.StartEvent import StartEvent
from ..specs.events.EndEvent import EndEvent
from ..specs.events.IntermediateEvent import (
BoundaryEvent,
_BoundaryEventParent,
EventBasedGateway,
from SpiffWorkflow.bpmn.specs.control import BpmnStartTask, _EndJoin, _BoundaryEventParent, SimpleBpmnTask
from SpiffWorkflow.bpmn.specs.bpmn_task_spec import _BpmnCondition
from SpiffWorkflow.bpmn.specs.defaults import (
UserTask,
ManualTask,
NoneTask,
ScriptTask,
ExclusiveGateway,
InclusiveGateway,
ParallelGateway,
StandardLoopTask,
SequentialMultiInstanceTask,
ParallelMultiInstanceTask,
CallActivity,
TransactionSubprocess,
SubWorkflowTask,
StartEvent,
EndEvent,
IntermediateCatchEvent,
IntermediateThrowEvent,
BoundaryEvent,
EventBasedGateway,
SendTask,
ReceiveTask,
)
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)
from .helpers.spec import TaskSpecConverter
class BpmnTaskSpecConverter(TaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
return dct
def from_dict(self, 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):
def __init__(self, registry):
super().__init__(NoneTask, registry)
@ -85,7 +92,6 @@ class ScriptTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['script'] = spec.script
return dct
@ -97,7 +103,6 @@ class StandardLoopTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct.update(self.get_standard_loop_attributes(spec))
return dct
@ -106,7 +111,6 @@ class MultiInstanceTaskConverter(BpmnTaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['task_spec'] = spec.task_spec
dct['cardinality'] = spec.cardinality
dct['data_input'] = self.registry.convert(spec.data_input)
@ -294,8 +298,8 @@ class EventBasedGatewayConverter(EventConverter):
DEFAULT_TASK_SPEC_CONVERTER_CLASSES = [
SimpleTaskConverter,
StartTaskConverter,
SimpleBpmnTaskConverter,
BpmnStartTaskConverter,
EndJoinConverter,
NoneTaskConverter,
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 gzip
from copy import deepcopy
from uuid import UUID
from ..workflow import BpmnMessage, BpmnWorkflow
from ..specs.SubWorkflowTask import SubWorkflowTask
from ...task import Task
from SpiffWorkflow.task import Task
from SpiffWorkflow.bpmn.workflow import BpmnMessage, BpmnWorkflow
from SpiffWorkflow.bpmn.specs.mixins.subworkflow_task import SubWorkflowTask
from .migration.version_migration import MIGRATIONS
from .helpers.registry import DefaultRegistry
@ -134,7 +153,7 @@ class BpmnWorkflowSerializer:
dct = self.__get_dict(serialization, use_gzip)
if self.VERSION_KEY in dct:
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
def workflow_to_dict(self, workflow):
@ -260,7 +279,7 @@ class BpmnWorkflowSerializer:
for child_task_id in task_dict['children']:
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)
else:
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
#
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,40 +18,20 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .event_types import ThrowingEvent, CatchingEvent
from ..BpmnSpecMixin import BpmnSpecMixin
from ....specs.Simple import Simple
from ....task import TaskState
class SendTask(ThrowingEvent):
@property
def spec_type(self):
return 'Send Task'
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.specs.StartTask import StartTask
from SpiffWorkflow.bpmn.specs.bpmn_task_spec import BpmnTaskSpec
from SpiffWorkflow.bpmn.specs.mixins.unstructured_join import UnstructuredJoin
from SpiffWorkflow.bpmn.specs.mixins.events.intermediate_event import BoundaryEvent
class ReceiveTask(CatchingEvent):
class BpmnStartTask(BpmnTaskSpec, StartTask):
pass
@property
def spec_type(self):
return 'Receive Task'
class SimpleBpmnTask(BpmnTaskSpec):
pass
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):
class _BoundaryEventParent(BpmnTaskSpec):
"""This task is inserted before a task with boundary events."""
# 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.
def __init__(self, wf_spec, name, main_child_task_spec, **kwargs):
super(_BoundaryEventParent, self).__init__(wf_spec, name)
super(_BoundaryEventParent, self).__init__(wf_spec, name, **kwargs)
self.main_child_task_spec = main_child_task_spec
@property
@ -93,39 +73,32 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin):
child._set_state(state)
class BoundaryEvent(CatchingEvent):
"""Task Spec for a bpmn:boundaryEvent node."""
class _EndJoin(UnstructuredJoin, BpmnTaskSpec):
def __init__(self, wf_spec, name, event_definition, cancel_activity, **kwargs):
"""
Constructor.
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
:param cancel_activity: True if this is a Cancelling boundary event.
"""
super(BoundaryEvent, self).__init__(wf_spec, name, event_definition, **kwargs)
self.cancel_activity = cancel_activity
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)
@property
def spec_type(self):
interrupting = 'Interrupting' if self.cancel_activity else 'Non-Interrupting'
return f'{interrupting} {self.event_definition.event_type} Event'
return force or len(waiting_tasks) == 0, waiting_tasks
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):
@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()
def _run_hook(self, my_task):
result = super(_EndJoin, self)._run_hook(my_task)
my_task.workflow.data.update(my_task.data)
return result

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
from copy import deepcopy
@ -8,13 +27,13 @@ data_log = logging.getLogger('spiff.data')
class BpmnDataSpecification:
def __init__(self, name, description=None):
def __init__(self, bpmn_id, bpmn_name=None):
"""
:param name: the variable (the BPMN ID)
:param description: a human readable name (the BPMN name)
"""
self.name = name
self.description = description or name
self.bpmn_id = bpmn_id
self.bpmn_name = bpmn_name
# In the future, we can add schemas defining the objects here.
def get(self, my_task, **kwargs):
@ -25,7 +44,7 @@ class 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 description: the task description (the BPMN name)
@ -35,7 +54,7 @@ class BpmnDataStoreSpecification(BpmnDataSpecification):
self.capacity = capacity or 0
self.is_unlimited = is_unlimited or True
# In the future, we can add schemas defining the objects here.
super().__init__(name, description)
super().__init__(bpmn_id, bpmn_name)
class BpmnIoSpecification:
@ -50,20 +69,20 @@ class DataObject(BpmnDataSpecification):
def get(self, my_task):
"""Copy a value form the workflow data to the task data."""
if self.name not in my_task.workflow.data:
message = f"The data object could not be read; '{self.name}' does not exist in the process."
if self.bpmn_id not in my_task.workflow.data:
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)
my_task.data[self.name] = deepcopy(my_task.workflow.data[self.name])
data_log.info(f'Read workflow variable {self.name}', extra=my_task.log_info())
my_task.data[self.bpmn_id] = deepcopy(my_task.workflow.data[self.bpmn_id])
data_log.info(f'Read workflow variable {self.bpmn_id}', extra=my_task.log_info())
def set(self, my_task):
"""Copy a value from the task data to the workflow data"""
if self.name not in my_task.data:
message = f"A data object could not be set; '{self.name}' not exist in the task."
if self.bpmn_id not in my_task.data:
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)
my_task.workflow.data[self.name] = deepcopy(my_task.data[self.name])
del my_task.data[self.name]
data_log.info(f'Set workflow variable {self.name}', extra=my_task.log_info())
my_task.workflow.data[self.bpmn_id] = deepcopy(my_task.data[self.bpmn_id])
del my_task.data[self.bpmn_id]
data_log.info(f'Set workflow variable {self.bpmn_id}', extra=my_task.log_info())
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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 SpiffWorkflow.exceptions import WorkflowException
from SpiffWorkflow.task import TaskState
seconds_from_utc = dstoffset if isdst else tzoffset
LOCALTZ = timezone(timedelta(seconds=-1 * seconds_from_utc))
@ -42,11 +41,12 @@ class EventDefinition(object):
and external flags.
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
# 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.
self.internal, self.external = True, True
self.description = description
@property
def event_type(self):
@ -92,8 +92,8 @@ class NamedEventDefinition(EventDefinition):
:param name: the name of this event
"""
def __init__(self, name):
super(NamedEventDefinition, self).__init__()
def __init__(self, name, **kwargs):
super(NamedEventDefinition, self).__init__(**kwargs)
self.name = name
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
of transaction subprocesses.
"""
def __init__(self):
super(CancelEventDefinition, self).__init__()
def __init__(self, **kwargs):
super(CancelEventDefinition, self).__init__(**kwargs)
self.internal = False
@property
def event_type(self):
return 'Cancel'
class ErrorEventDefinition(NamedEventDefinition):
"""
@ -123,15 +119,11 @@ class ErrorEventDefinition(NamedEventDefinition):
matched by code rather than name.
"""
def __init__(self, name, error_code=None):
super(ErrorEventDefinition, self).__init__(name)
def __init__(self, name, error_code=None, **kwargs):
super(ErrorEventDefinition, self).__init__(name,**kwargs)
self.error_code = error_code
self.internal = False
@property
def event_type(self):
return 'Error'
def __eq__(self, other):
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.
"""
def __init__(self, name, escalation_code=None):
def __init__(self, name, escalation_code=None, **kwargs):
"""
Constructor.
:param escalation_code: The escalation code this event should
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
@property
def event_type(self):
return 'Escalation'
def __eq__(self, other):
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):
"""The default message event."""
def __init__(self, name, correlation_properties=None):
super().__init__(name)
def __init__(self, name, correlation_properties=None, **kwargs):
super().__init__(name, **kwargs)
self.correlation_properties = correlation_properties or []
self.payload = None
self.internal = False
@property
def event_type(self):
return 'Message'
def catch(self, my_task, event_definition = None):
def catch(self, my_task, event_definition=None):
self.update_internal_data(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.
"""
def __init__(self):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.internal, self.external = False, False
@property
def event_type(self):
return 'Default'
def throw(self, my_task):
"""It's a 'none' event, so nothing to throw."""
pass
@ -261,26 +242,21 @@ class NoneEventDefinition(EventDefinition):
class SignalEventDefinition(NamedEventDefinition):
"""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):
"""The TerminateEventDefinition is the implementation of event definition used for Termination Events."""
def __init__(self):
super(TerminateEventDefinition, self).__init__()
def __init__(self, **kwargs):
super(TerminateEventDefinition, self).__init__(**kwargs)
self.external = False
@property
def event_type(self):
return 'Terminate'
class TimerEventDefinition(EventDefinition):
def __init__(self, name, expression):
def __init__(self, name, expression, **kwargs):
"""
Constructor.
@ -288,7 +264,7 @@ class TimerEventDefinition(EventDefinition):
:param expression: An ISO 8601 datetime or interval expression.
"""
super().__init__()
super().__init__(**kwargs)
self.name = name
self.expression = expression
@ -362,7 +338,7 @@ class TimerEventDefinition(EventDefinition):
@staticmethod
def parse_iso_week(expression):
# 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()
ds = datetime.fromisocalendar(int(year), int(month), int(day)).strftime('%Y-%m-%d')
return TimerEventDefinition.get_datetime(ds + (ts or ''))
@ -409,10 +385,6 @@ class TimerEventDefinition(EventDefinition):
class TimeDateEventDefinition(TimerEventDefinition):
"""A Timer event represented by a specific date/time."""
@property
def event_type(self):
return 'Time Date Timer'
def has_fired(self, my_task):
event_value = my_task._get_internal_data('event_value')
if event_value is None:
@ -429,10 +401,6 @@ class TimeDateEventDefinition(TimerEventDefinition):
class DurationTimerEventDefinition(TimerEventDefinition):
"""A timer event represented by a duration"""
@property
def event_type(self):
return 'Duration Timer'
def has_fired(self, my_task):
event_value = my_task._get_internal_data("event_value")
if event_value is None:
@ -450,10 +418,6 @@ class DurationTimerEventDefinition(TimerEventDefinition):
class CycleTimerEventDefinition(TimerEventDefinition):
@property
def event_type(self):
return 'Cycle Timer'
def cycle_complete(self, my_task):
event_value = my_task._get_internal_data('event_value')
@ -489,15 +453,11 @@ class CycleTimerEventDefinition(TimerEventDefinition):
class MultipleEventDefinition(EventDefinition):
def __init__(self, event_definitions=None, parallel=False):
super().__init__()
def __init__(self, event_definitions=None, parallel=False, **kwargs):
super().__init__(**kwargs)
self.event_definitions = event_definitions or []
self.parallel = parallel
@property
def event_type(self):
return 'Multiple'
def has_fired(self, my_task):
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,9 +17,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.task import TaskState
from .event_types import ThrowingEvent
from .event_definitions import TerminateEventDefinition, CancelEventDefinition
from ....task import TaskState
from ...event_definitions import TerminateEventDefinition, CancelEventDefinition
class EndEvent(ThrowingEvent):
@ -41,14 +41,6 @@ class EndEvent(ThrowingEvent):
Gateways, one of the associated Events has been triggered.
* 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):
super(EndEvent, self)._on_complete_hook(my_task)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,22 +16,22 @@
# 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 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."""
def __init__(self, wf_spec, name, event_definition, **kwargs):
def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs):
"""
Constructor.
: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
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.
"""
self.event_definition.catch(my_task, event_definition)
my_task.last_update_time = time.time()
my_task._set_state(TaskState.WAITING)
def _update_hook(self, my_task):
@ -74,26 +75,17 @@ class CatchingEvent(Simple, BpmnSpecMixin):
self.event_definition.reset(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):
return True
def task_will_set_children_future(self, my_task):
my_task.internal_data = {}
class ThrowingEvent(Simple, BpmnSpecMixin):
class ThrowingEvent(TaskSpec):
"""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.
: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
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,27 +17,16 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.task import TaskState
from .event_types import CatchingEvent
from ....task import TaskState
class StartEvent(CatchingEvent):
"""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):
# 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 my_task.state == TaskState.COMPLETED or my_task.state == TaskState.CANCELLED:
my_task.set_children_future()
my_task._set_state(TaskState.WAITING)
my_task.workflow.reset_from_task_id(my_task.id)
super(StartEvent, self).catch(my_task, event_definition)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,12 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin
from ...specs.ExclusiveChoice import ExclusiveChoice
from ...specs.MultiChoice import MultiChoice
from SpiffWorkflow.specs.ExclusiveChoice import ExclusiveChoice
from SpiffWorkflow.specs.MultiChoice import MultiChoice
class ExclusiveGateway(ExclusiveChoice, BpmnSpecMixin):
class ExclusiveGateway(ExclusiveChoice):
"""
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
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,10 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from SpiffWorkflow.exceptions import WorkflowTaskException
from ...task import TaskState
from .UnstructuredJoin import UnstructuredJoin
from ...specs.MultiChoice import MultiChoice
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.specs.MultiChoice import MultiChoice
from .unstructured_join import UnstructuredJoin
class InclusiveGateway(MultiChoice, UnstructuredJoin):
@ -113,10 +114,6 @@ class InclusiveGateway(MultiChoice, UnstructuredJoin):
def _run_hook(self, my_task):
outputs = self._get_matching_outputs(my_task)
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)
return True
@property
def spec_type(self):
return 'Inclusive Gateway'

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,16 +16,13 @@
# 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.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):
return False
@property
def spec_type(self):
return 'Manual Task'
def __init__(self, wf_spec, bpmn_id, **kwargs):
super().__init__(wf_spec, bpmn_id, **kwargs)
self.manual = True

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2020 Sartography
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -20,13 +20,13 @@
from copy import deepcopy
from collections.abc import Iterable, Sequence, Mapping, MutableSequence, MutableMapping
from ...task import TaskState
from ...util.deep_merge import DeepMerge
from ..exceptions import WorkflowDataException
from .BpmnSpecMixin import BpmnSpecMixin
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.specs.base import TaskSpec
from SpiffWorkflow.util.deep_merge import DeepMerge
from SpiffWorkflow.bpmn.exceptions import WorkflowDataException
class LoopTask(BpmnSpecMixin):
class LoopTask(TaskSpec):
def process_children(self, my_task):
"""
@ -50,8 +50,8 @@ class LoopTask(BpmnSpecMixin):
class StandardLoopTask(LoopTask):
def __init__(self, wf_spec, name, task_spec, maximum, condition, test_before, **kwargs):
super().__init__(wf_spec, name, **kwargs)
def __init__(self, wf_spec, bpmn_id, task_spec, maximum, condition, test_before, **kwargs):
super().__init__(wf_spec, bpmn_id, **kwargs)
self.task_spec = task_spec
self.maximum = maximum
self.condition = condition
@ -59,7 +59,9 @@ class StandardLoopTask(LoopTask):
def _update_hook(self, my_task):
if my_task.state != TaskState.WAITING:
super()._update_hook(my_task)
child_running = self.process_children(my_task)
if child_running:
# 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:
my_task._set_state(TaskState.WAITING)
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)
def child_completed_action(self, my_task, child):
@ -92,11 +94,11 @@ class StandardLoopTask(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,
**kwargs):
super().__init__(wf_spec, name, **kwargs)
super().__init__(wf_spec, bpmn_id, **kwargs)
self.task_spec = task_spec
self.cardinality = cardinality
self.data_input = data_input
@ -109,13 +111,13 @@ class MultiInstanceTask(LoopTask):
"""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.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)
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')
data_output = my_task.data[self.data_output.name]
data_input = my_task.data[self.data_input.name] if self.data_input is not None else None
if isinstance(data_output, Mapping) or data_input is data_output:
data_output = my_task.data[self.data_output.bpmn_id]
data_input = my_task.data[self.data_input.bpmn_id] if self.data_input is not None else None
if key_or_index is not None and (isinstance(data_output, Mapping) or data_input is data_output):
data_output[key_or_index] = item
else:
data_output.append(item)
@ -128,7 +130,7 @@ class MultiInstanceTask(LoopTask):
child = my_task._add_child(task_spec, TaskState.WAITING)
child.data = deepcopy(my_task.data)
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:
child.internal_data['key_or_index'] = key_or_index
child.task_spec._update(child)
@ -142,7 +144,7 @@ class MultiInstanceTask(LoopTask):
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 isinstance(input_data, (MutableMapping, MutableSequence)):
# 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
my_task.data[name] = list()
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)):
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:
@ -163,7 +165,7 @@ class MultiInstanceTask(LoopTask):
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:
my_task.data[name] = list()
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):
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')
if remaining is None:
@ -229,9 +231,9 @@ class SequentialMultiInstanceTask(MultiInstanceTask):
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)
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
if isinstance(input_data, Sequence):
@ -287,7 +289,7 @@ class ParallelMultiInstanceTask(MultiInstanceTask):
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:
# 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):
@ -306,7 +308,7 @@ class ParallelMultiInstanceTask(MultiInstanceTask):
if self.data_output 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:
self.init_data_output_with_cardinality(my_task)

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,16 +16,13 @@
# 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.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):
return False
@property
def spec_type(self):
return 'Task'
def __init__(self, wf_spec, bpmn_id, **kwargs):
super().__init__(wf_spec, bpmn_id, **kwargs)
self.manual = True

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2012 Matthew Hampton
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,7 +16,8 @@
# 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 .UnstructuredJoin import UnstructuredJoin
from .unstructured_join import UnstructuredJoin
class ParallelGateway(UnstructuredJoin):
@ -43,7 +44,3 @@ class ParallelGateway(UnstructuredJoin):
def _check_threshold_unstructured(self, my_task, force=False):
completed_inputs, waiting_tasks = self._get_inputs_with_tokens(my_task)
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,11 +17,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin
from ...specs.Simple import Simple
from SpiffWorkflow.specs.base import TaskSpec
class ScriptEngineTask(Simple, BpmnSpecMixin):
class ScriptEngineTask(TaskSpec):
"""Task Spec for a bpmn:scriptTask node"""
def _execute(self, task):
@ -34,18 +33,14 @@ class ScriptEngineTask(Simple, BpmnSpecMixin):
class ScriptTask(ScriptEngineTask):
def __init__(self, wf_spec, name, script, **kwargs):
def __init__(self, wf_spec, bpmn_id, script, **kwargs):
"""
Constructor.
: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
@property
def spec_type(self):
return 'Script Task'
def _execute(self, task):
return task.workflow.script_engine.execute(task, self.script)

View File

@ -1,13 +1,13 @@
# -*- 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,19 +17,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from .BpmnSpecMixin import BpmnSpecMixin
from ...specs.Simple import Simple
from .script_task import ScriptEngineTask
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):
return False
@property
def spec_type(self):
return 'User Task'
def __init__(self, wf_spec, bpmn_id, **kwargs):
super(ServiceTask, self).__init__(wf_spec, bpmn_id, **kwargs)

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 SpiffWorkflow.task import TaskState
from .BpmnSpecMixin import BpmnSpecMixin
from ..exceptions import WorkflowDataException
from SpiffWorkflow.specs.base import TaskSpec
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.
"""
def __init__(self, wf_spec, name, subworkflow_spec, transaction=False, **kwargs):
def __init__(self, wf_spec, bpmn_id, subworkflow_spec, transaction=False, **kwargs):
"""
Constructor.
:param bpmn_wf_spec: the BpmnProcessSpec for the sub process.
: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.transaction = transaction
@property
def spec_type(self):
return 'Subprocess'
def _on_subworkflow_completed(self, subworkflow, my_task):
self.update_data(my_task, subworkflow)
@ -68,14 +85,11 @@ class SubWorkflowTask(BpmnSpecMixin):
child.task_spec._update(child)
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):
def __init__(self, wf_spec, name, subworkflow_spec, **kwargs):
super(CallActivity, self).__init__(wf_spec, name, subworkflow_spec, False, **kwargs)
def __init__(self, wf_spec, bpmn_id, subworkflow_spec, **kwargs):
super(CallActivity, self).__init__(wf_spec, bpmn_id, subworkflow_spec, False, **kwargs)
def copy_data(self, my_task, subworkflow):
@ -86,13 +100,13 @@ class CallActivity(SubWorkflowTask):
else:
# Otherwise copy only task data with the specified names
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(
"You are missing a required Data Input for a call activity.",
task=my_task,
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):
@ -103,25 +117,36 @@ class CallActivity(SubWorkflowTask):
end = subworkflow.get_tasks_from_spec_name('End', workflow=subworkflow)
# Otherwise only copy data with the specified names
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(
f"The Data Output was not available in the subprocess output.",
"The Data Output was not available in the subprocess output.",
task=my_task,
data_output=var,
)
my_task.data[var.name] = end[0].data[var.name]
@property
def spec_type(self):
return 'Call Activity'
my_task.data[var.bpmn_id] = end[0].data[var.bpmn_id]
class TransactionSubprocess(SubWorkflowTask):
def __init__(self, wf_spec, name, subworkflow_spec, **kwargs):
super(TransactionSubprocess, self).__init__(wf_spec, name, subworkflow_spec, True, **kwargs)
@property
def spec_type(self):
return 'Transactional Subprocess'
def __init__(self, wf_spec, bpmn_id, subworkflow_spec, **kwargs):
super(TransactionSubprocess, self).__init__(wf_spec, bpmn_id, subworkflow_spec, True, **kwargs)
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -17,12 +17,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
from ...task import TaskState
from .BpmnSpecMixin import BpmnSpecMixin
from ...specs.Join import Join
from SpiffWorkflow.task import TaskState
from SpiffWorkflow.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
for the BPMN style threading
@ -83,13 +82,3 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
task._drop_children()
else:
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
# Copyright (C) 2012 Matthew Hampton, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -15,21 +16,26 @@
# 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
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,
MultipleEventDefinition,
NamedEventDefinition,
TimerEventDefinition,
)
from SpiffWorkflow.bpmn.specs.control import _BoundaryEventParent
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:
@ -86,6 +92,7 @@ class BpmnWorkflow(Workflow):
def delete_subprocess(self, my_task):
workflow = self._get_outermost_workflow(my_task)
if my_task.id in workflow.subprocesses:
del workflow.subprocesses[my_task.id]
def get_subprocess(self, my_task):
@ -94,7 +101,17 @@ class BpmnWorkflow(Workflow):
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
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)
task = Task(self, new)
start = self.get_tasks_from_spec_name('Start', workflow=self)[0]
@ -135,7 +152,7 @@ class BpmnWorkflow(Workflow):
:param event_definition: the thrown event
"""
# 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.
for name, spec in self.subprocess_specs.items():
for task_spec in list(spec.task_specs.values()):
@ -183,8 +200,8 @@ class BpmnWorkflow(Workflow):
conversation = task.task_spec.event_definition.conversation()
if not conversation:
raise WorkflowTaskException(
f"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)
"The waiting task and message payload can not be matched to any correlation key (conversation topic). "
"And is therefor unable to respond to the given message.", task)
updated_props = self._correlate(conversation, payload, task)
task.task_spec.catch(task, event_definition)
self.refresh_waiting_tasks()
@ -229,7 +246,7 @@ class BpmnWorkflow(Workflow):
elif isinstance(event_definition, MessageEventDefinition):
value = event_definition.correlation_properties
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,
'value': value
})
@ -246,7 +263,7 @@ class BpmnWorkflow(Workflow):
: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
"""
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:
for task in engine_steps:
if will_complete_task is not None:
@ -256,7 +273,7 @@ class BpmnWorkflow(Workflow):
did_complete_task(task)
if task.task_spec.name == exit_at:
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,
will_refresh_task=None,
@ -292,10 +309,10 @@ class BpmnWorkflow(Workflow):
# almost surely be in a different state than the tasks we want
for task in Workflow.get_tasks_iterator(wf):
subprocess = top.subprocesses.get(task.id)
if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess))
if task._has_state(state):
tasks.append(task)
if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess))
return tasks
def get_task_from_id(self, task_id, workflow=None):
@ -308,11 +325,9 @@ class BpmnWorkflow(Workflow):
"""Returns a list of User Tasks that are READY for user action"""
if lane is not None:
return [t for t in self.get_tasks(TaskState.READY, workflow)
if (not self._is_engine_task(t.task_spec))
and (t.task_spec.lane == lane)]
if t.task_spec.manual and t.task_spec.lane == lane]
else:
return [t for t in self.get_tasks(TaskState.READY, workflow)
if not self._is_engine_task(t.task_spec)]
return [t for t in self.get_tasks(TaskState.READY, workflow) if t.task_spec.manual]
def get_waiting_tasks(self, workflow=None):
"""Returns a list of all WAITING tasks"""
@ -321,5 +336,58 @@ class BpmnWorkflow(Workflow):
def get_catching_tasks(self, workflow=None):
return [task for task in self.get_tasks(workflow=workflow) if isinstance(task.task_spec, CatchingEvent)]
def _is_engine_task(self, task_spec):
return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task())
def reset_from_task_id(self, task_id, data=None):
"""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 @@
from SpiffWorkflow.bpmn.parser.BpmnParser import full_tag, DEFAULT_NSMAP
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.NoneTask import NoneTask
from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask
from SpiffWorkflow.bpmn.specs.SubWorkflowTask import CallActivity, TransactionSubprocess
# 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.dmn.parser.BpmnDmnParser import BpmnDmnParser
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.camunda.specs.UserTask import UserTask
from SpiffWorkflow.bpmn.parser.BpmnParser import full_tag, DEFAULT_NSMAP
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 (
CamundaTaskParser,
BusinessRuleTaskParser,
@ -18,10 +44,6 @@ from SpiffWorkflow.camunda.parser.task_spec import (
ScriptTaskParser,
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 (
CamundaStartEventParser,
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
from SpiffWorkflow.bpmn.parser.event_parsers import StartEventParser, EndEventParser, \
IntermediateCatchEventParser, IntermediateThrowEventParser, BoundaryEventParser
from SpiffWorkflow.camunda.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.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

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.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.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.user_task import Form, FormField, EnumFormField
CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn'
@ -66,11 +83,9 @@ class BusinessRuleTaskParser(CamundaTaskParser):
def create_task(self):
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),
lane=self.lane, position=self.position,
description=self.node.get('name', None),
)
**self.bpmn_attributes)
@staticmethod
def get_decision_ref(node):
@ -82,10 +97,7 @@ class UserTaskParser(CamundaTaskParser):
def create_task(self):
form = self.get_form()
return self.spec_class(self.spec, self.get_task_spec_name(), form,
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, form=form, **self.bpmn_attributes)
def get_form(self):
"""Camunda provides a simple form builder, this will extract the
@ -138,10 +150,7 @@ class SubWorkflowParser(CamundaTaskParser):
def create_task(self):
subworkflow_spec = SubprocessParser.get_subprocess_spec(self)
return self.spec_class(
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
class CallActivityParser(CamundaTaskParser):
@ -149,10 +158,7 @@ class CallActivityParser(CamundaTaskParser):
def create_task(self):
subworkflow_spec = SubprocessParser.get_call_activity_spec(self)
return self.spec_class(
self.spec, self.get_task_spec_name(), subworkflow_spec,
lane=self.lane, position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, subworkflow_spec=subworkflow_spec, **self.bpmn_attributes)
class ScriptTaskParser(TaskParser):
@ -162,10 +168,7 @@ class ScriptTaskParser(TaskParser):
def create_task(self):
script = self.get_script()
return self.spec_class(self.spec, self.get_task_spec_name(), script,
lane=self.lane,
position=self.position,
description=self.node.get('name', None))
return self.spec_class(self.spec, self.bpmn_id, script=script, **self.bpmn_attributes)
def get_script(self):
"""
@ -177,5 +180,5 @@ class ScriptTaskParser(TaskParser):
return one(self.xpath('.//bpmn:script')).text
except AssertionError as ae:
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)

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 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 .task_spec import UserTaskConverter, ParallelMultiInstanceTaskConverter, SequentialMultiInstanceTaskConverter
from .task_spec import (
UserTaskConverter,
BusinessRuleTaskConverter,
ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter
)
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'].remove(DefaultSequentialMIConverter)
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'].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 ..specs.events.event_definitions import MessageEventDefinition
from ..specs.event_definitions import MessageEventDefinition
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.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
class UserTaskConverter(TaskSpecConverter):
@ -11,7 +32,6 @@ class UserTaskConverter(TaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
dct['form'] = self.form_to_dict(spec.form)
return dct
@ -36,6 +56,10 @@ class UserTaskConverter(TaskSpecConverter):
return dct
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
class ParallelMultiInstanceTaskConverter(MultiInstanceTaskConverter):
def __init__(self, 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):
"""
@ -10,9 +29,9 @@ class MessageEventDefinition(MessageEventDefinition):
# this should be revisited: for one thing, we're relying on some Camunda-specific
# 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.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.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.MultiInstanceTask import (
from SpiffWorkflow.bpmn.specs.defaults import (
SequentialMultiInstanceTask as BpmnSequentialMITask,
ParallelMultiInstanceTask as BpmnParallelMITask,
)
@ -20,19 +39,19 @@ def update_task_spec(my_task):
if task_spec.cardinality is None:
# Use the same collection for input and output
task_spec.data_input = TaskDataReference(task_spec.data_output.name)
task_spec.input_item = TaskDataReference(task_spec.output_item.name)
task_spec.data_input = TaskDataReference(task_spec.data_output.bpmn_id)
task_spec.input_item = TaskDataReference(task_spec.output_item.bpmn_id)
else:
cardinality = my_task.workflow.script_engine.evaluate(my_task, task_spec.cardinality)
if not isinstance(cardinality, int):
# The input data was supplied via "cardinality"
# We'll use the same reference for input and output item
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
else:
# 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):

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
from ...bpmn.specs.BpmnSpecMixin import BpmnSpecMixin
class UserTask(UserTask, BpmnSpecMixin):
class UserTask(DefaultUserTask):
"""Task Spec for a bpmn:userTask node with Camunda forms."""
def __init__(self, wf_spec, name, form, **kwargs):
@ -17,12 +32,6 @@ class UserTask(UserTask, BpmnSpecMixin):
super(UserTask, self).__init__(wf_spec, name, **kwargs)
self.form = form
def _on_trigger(self, my_task):
pass
def is_engine_task(self):
return False
class FormField(object):
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 re
from SpiffWorkflow.exceptions import SpiffWorkflowException
from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException
from ..specs.model import HitPolicy
from ...exceptions import SpiffWorkflowException, WorkflowTaskException
from ...util import levenshtein
from ...workflow import WorkflowException
logger = logging.getLogger('spiff.dmn')
@ -37,7 +56,7 @@ class DMNEngine:
for rule in matched_rules:
rule_output = rule.output_as_dict(task)
for key in rule_output.keys():
if not key in result:
if key not in result:
result[key] = []
result[key].append(rule_output[key])
elif len(matched_rules) > 0:
@ -98,7 +117,7 @@ class DMNEngine:
# NOTE: It should only do this replacement outside of quotes.
# for example, provided "This thing?" in quotes, it should not
# do the replacement.
match_expr = re.sub('(\?)(?=(?:[^\'"]|[\'"][^\'"]*[\'"])*$)', 'dmninputexpr', match_expr)
match_expr = re.sub(r'(\?)(?=(?:[^\'"]|[\'"][^\'"]*[\'"])*$)', 'dmninputexpr', match_expr)
if 'dmninputexpr' in match_expr:
external_methods = {
'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 os
@ -53,7 +72,7 @@ class BpmnDmnParser(BpmnParser):
validator.validate(node, 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
def add_dmn_file(self, filename):
@ -75,7 +94,19 @@ class BpmnDmnParser(BpmnParser):
"""
for filename in filenames:
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):
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
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, \
OutputEntry, Input, Output, Rule
from SpiffWorkflow.dmn.specs.model import (
Decision,
DecisionTable,
InputEntry,
OutputEntry,
Input,
Output,
Rule,
)
def get_dmn_ns(node):
"""
@ -55,7 +81,8 @@ class DMNParser(NodeParser):
def parse(self):
self.decision = self._parse_decision(self.node.findall('{*}decision'))
def get_id(self):
@property
def bpmn_id(self):
"""
Returns the process ID
"""
@ -172,9 +199,7 @@ class DMNParser(NodeParser):
return rule
def _parse_input_output_element(self, decision_table, element, cls, idx):
input_or_output = (
decision_table.inputs if cls == InputEntry else decision_table.outputs if cls == OutputEntry else None)[
idx]
input_or_output = (decision_table.inputs if cls == InputEntry else decision_table.outputs)[idx]
entry = cls(element.attrib['id'], input_or_output)
for child in element:
if child.tag.endswith('description'):
@ -182,7 +207,8 @@ class DMNParser(NodeParser):
elif child.tag.endswith('text'):
entry.text = child.text
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:
if entry.text and entry.text != '':
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 ..specs.BusinessRuleTask import BusinessRuleTask
from ..specs.model import DecisionTable, Rule, HitPolicy
from ..specs.model import Input, InputEntry, Output, OutputEntry
from ..engine.DMNEngine import DMNEngine
@ -9,7 +27,6 @@ class BaseBusinessRuleTaskConverter(TaskSpecConverter):
def to_dict(self, spec):
dct = self.get_default_attributes(spec)
dct.update(self.get_bpmn_attributes(spec))
# We only ever use one decision table
dct['decision_table'] = self.decision_table_to_dict(spec.dmnEngine.decision_table)
return dct
@ -95,8 +112,3 @@ class BaseBusinessRuleTaskConverter(TaskSpecConverter):
rule.outputEntries = [self.output_entry_from_dict(entry, outputs)
for entry in dct['output_entries']]
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 enum import Enum

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007 Samuel Abels
# Copyright (C) 2007 Samuel Abels, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -15,10 +16,6 @@
# 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
class SpiffWorkflowException(Exception):
"""
@ -55,81 +52,6 @@ class WorkflowException(SpiffWorkflowException):
# Points to the TaskSpec that generated the exception.
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):
pass

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,6 +16,7 @@
# 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 re
@ -198,8 +199,8 @@ def valueof(scope, op, default=None):
def is_number(text):
try:
x = int(text)
except:
int(text)
except Exception:
return False
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 -*-
from builtins import object
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -20,7 +19,6 @@ from .. import operators
from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute
@ -46,7 +44,6 @@ def spec_map():
'acquire-mutex': AcquireMutex,
'cancel': Cancel,
'cancel-task': CancelTask,
'celery': Celery,
'choose': Choose,
'exclusive-choice': ExclusiveChoice,
'execute': Execute,

View File

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
import json
from builtins import str
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import json
import pickle
from base64 import b64encode, b64decode
from ..workflow import Workflow
from ..util.impl import get_class
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.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute
@ -51,25 +50,17 @@ import warnings
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):
return dict(
(str(k), b64encode(pickle.dumps(v,
protocol=pickle.HIGHEST_PROTOCOL)))
for k, v in list(thedict.items()))
(str(k), b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)))
for k, v in list(thedict.items())
)
def deserialize_dict(self, s_state):
return dict((k, pickle.loads(b64decode(v)))
for k, v in list(s_state.items()))
return dict((k, pickle.loads(b64decode(v))) for k, v in list(s_state.items()))
def serialize_list(self, thelist):
return [b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL))
for v in thelist]
return [b64encode(pickle.dumps(v, protocol=pickle.HIGHEST_PROTOCOL)) for v in thelist]
def deserialize_list(self, s_state):
return [pickle.loads(b64decode(v)) for v in s_state]
@ -149,48 +140,34 @@ class DictionarySerializer(Serializer):
return ret
def serialize_task_spec(self, spec):
s_state = dict(id=spec.id,
name=spec.name,
s_state = dict(name=spec.name,
description=spec.description,
manual=spec.manual,
internal=spec.internal,
lookahead=spec.lookahead)
module_name = spec.__class__.__module__
s_state['class'] = module_name + '.' + spec.__class__.__name__
s_state['inputs'] = [t.id for t in spec.inputs]
s_state['outputs'] = [t.id for t in spec.outputs]
s_state['inputs'] = [t.name for t in spec.inputs]
s_state['outputs'] = [t.name for t in spec.outputs]
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['pre_assign'] = self.serialize_list(spec.pre_assign)
s_state['post_assign'] = self.serialize_list(spec.post_assign)
# Note: Events are not serialized; this is documented in
# the TaskSpec API docs.
return s_state
def deserialize_task_spec(self, wf_spec, s_state, spec):
spec.id = s_state.get('id', None)
spec.description = s_state.get('description', '')
spec.manual = s_state.get('manual', False)
spec.internal = s_state.get('internal', False)
spec.lookahead = s_state.get('lookahead', 2)
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.pre_assign = self.deserialize_list(s_state.get('pre_assign', []))
spec.post_assign = self.deserialize_list(
s_state.get('post_assign', []))
spec.post_assign = self.deserialize_list(s_state.get('post_assign', []))
# 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.
spec.inputs = s_state.get('inputs', [])[:]
spec.outputs = s_state.get('outputs', [])[:]
return spec
def serialize_acquire_mutex(self, spec):
@ -210,8 +187,7 @@ class DictionarySerializer(Serializer):
return s_state
def deserialize_cancel(self, wf_spec, s_state):
spec = Cancel(wf_spec, s_state['name'],
success=s_state.get('cancel_successfully', False))
spec = Cancel(wf_spec, s_state['name'], success=s_state.get('cancel_successfully', False))
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec
@ -226,26 +202,6 @@ class DictionarySerializer(Serializer):
self.deserialize_task_spec(wf_spec, s_state, spec=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):
s_state = self.serialize_task_spec(spec)
s_state['context'] = spec.context
@ -305,12 +261,12 @@ class DictionarySerializer(Serializer):
s_state['cancel_remaining'] = spec.cancel_remaining
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):
byte_payload = s_state['threshold']['__bytes__']
else:
byte_payload = s_state['threshold']
spec = cls(wf_spec,
spec = Join(wf_spec,
s_state['name'],
split_task=s_state['split_task'],
threshold=pickle.loads(b64decode(byte_payload)),
@ -347,36 +303,25 @@ class DictionarySerializer(Serializer):
s_state = self.serialize_task_spec(spec)
# here we need to add in all of the things that would get serialized
# for other classes that the MultiInstance could be -
#
if isinstance(spec, SubWorkflow):
br_state = self.serialize_sub_workflow(spec)
s_state['file'] = br_state['file']
s_state['in_assign'] = br_state['in_assign']
s_state['out_assign'] = br_state['out_assign']
s_state['times'] = self.serialize_arg(spec.times)
s_state['prevtaskclass'] = spec.prevtaskclass
return s_state
def deserialize_multi_instance(self, wf_spec, s_state, cls=None):
if cls == None:
cls = MultiInstance(wf_spec,
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):
def deserialize_multi_instance(self, wf_spec, s_state):
spec = MultiInstance(wf_spec, s_state['name'], times=self.deserialize_arg(s_state['times']))
if isinstance(spec, SubWorkflow):
if s_state.get('file'):
cls.file = self.deserialize_arg(s_state['file'])
spec.file = self.deserialize_arg(s_state['file'])
else:
cls.file = None
cls.in_assign = self.deserialize_list(s_state['in_assign'])
cls.out_assign = self.deserialize_list(s_state['out_assign'])
self.deserialize_task_spec(wf_spec, s_state, spec=cls)
return cls
spec.file = None
spec.in_assign = self.deserialize_list(s_state['in_assign'])
spec.out_assign = self.deserialize_list(s_state['out_assign'])
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec
def serialize_release_mutex(self, 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)
return spec
def deserialize_generic(self, wf_spec, s_state,newclass):
assert isinstance(wf_spec, WorkflowSpec)
spec = newclass(wf_spec, s_state['name'])
@ -486,70 +430,32 @@ class DictionarySerializer(Serializer):
return spec
def serialize_workflow_spec(self, spec, **kwargs):
s_state = dict(name=spec.name,
description=spec.description,
file=spec.file)
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)
s_state = dict(name=spec.name, description=spec.description, file=spec.file)
s_state['task_specs'] = dict(
(k, v.serialize(self))
for k, v in list(spec.task_specs.items())
)
return s_state
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.outputs = [spec.get_task_spec_from_id(t) for t in task_spec.outputs]
def _prevtaskclass_bases(self, oldtask):
return (oldtask)
task_spec.inputs = [spec.get_task_spec_from_name(t) for t in task_spec.inputs]
task_spec.outputs = [spec.get_task_spec_from_name(t) for t in task_spec.outputs]
def deserialize_workflow_spec(self, s_state, **kwargs):
spec = WorkflowSpec(s_state['name'], filename=s_state['file'])
spec.description = s_state['description']
# Handle Start Task
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']
start_task_spec_state = s_state['task_specs']['Start']
start_task_spec = StartTask.deserialize(self, spec, start_task_spec_state)
spec.start = start_task_spec
spec.task_specs['Start'] = start_task_spec
for name, task_spec_state in list(s_state['task_specs'].items()):
if name == 'Start':
continue
prevtask = task_spec_state.get('prevtaskclass', None)
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)
spec.task_specs[name] = task_spec
@ -558,7 +464,7 @@ class DictionarySerializer(Serializer):
self._deserialize_workflow_spec_task_spec(spec, task_spec, name)
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')
return spec
@ -578,15 +484,11 @@ class DictionarySerializer(Serializer):
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
workflow_spec, otherwise the spec is assumed to be serialized in the
s_state['wf_spec']"""
if wf_spec is None:
# The json serializer serializes the spec as a string and then serializes it again, hence this check
# 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:
@ -595,8 +497,6 @@ class DictionarySerializer(Serializer):
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:
reset_specs = []
workflow = wf_class(wf_spec)
workflow.data = self.deserialize_dict(s_state['data'])
@ -623,35 +523,24 @@ class DictionarySerializer(Serializer):
return workflow
def serialize_task(self, task, skip_children=False, allow_subs=False):
"""
:param allow_subs: Allows sub-serialization to take place, otherwise
assumes that the subworkflow is stored in internal data and raises an error.
"""
def serialize_task(self, task, skip_children=False):
assert isinstance(task, Task)
# 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):
if isinstance(task.task_spec, SubWorkflow):
raise TaskNotSupportedError(
"Subworkflow tasks cannot be serialized (due to their use of" +
" internal_data to store the subworkflow).")
s_state = dict()
s_state['id'] = task.id
s_state['workflow_name'] = task.workflow.name
s_state['parent'] = task.parent.id if task.parent is not None else None
if not skip_children:
s_state['children'] = [
self.serialize_task(child) for child in task.children]
s_state['children'] = [self.serialize_task(child) for child in task.children]
s_state['state'] = task.state
s_state['triggered'] = task.triggered
s_state['task_spec'] = task.task_spec.name
s_state['last_state_change'] = task.last_state_change
s_state['data'] = self.serialize_dict(task.data)
s_state['internal_data'] = task.internal_data
return s_state
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)
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']
# as the task_tree might not be complete yet
# keep the ids so they can be processed at the end

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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):
pass

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -14,6 +14,7 @@
# 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 uuid
from ..operators import Attrib

View File

@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2007-2012 Samuel Abels
# Copyright (C) 2007-2012 Samuel Abels, 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
@ -16,6 +16,7 @@
# 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 .. import operators
from ..specs.Simple import Simple
from ..specs.WorkflowSpec import WorkflowSpec

View File

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
from builtins import str
# 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
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# 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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA
import warnings
from lxml import etree
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.Cancel import Cancel
from ..specs.CancelTask import CancelTask
from ..specs.Celery import Celery
from ..specs.Choose import Choose
from ..specs.ExclusiveChoice import ExclusiveChoice
from ..specs.Execute import Execute
@ -287,15 +286,11 @@ class XmlSerializer(Serializer):
"""
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
if spec.description:
SubElement(elem, 'description').text = spec.description
if spec.manual:
SubElement(elem, 'manual')
if spec.internal:
SubElement(elem, 'internal')
SubElement(elem, 'lookahead').text = str(spec.lookahead)
inputs = [t.name for t in spec.inputs]
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):
name = elem.findtext('name')
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.manual = elem.findtext('manual', spec.manual)
spec.internal = elem.find('internal') is not None
spec.lookahead = int(elem.findtext('lookahead', spec.lookahead))
data_elem = elem.find('data')
@ -384,37 +376,6 @@ class XmlSerializer(Serializer):
def deserialize_cancel_task(self, wf_spec, elem, cls=CancelTask, **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):
if elem is None:
elem = etree.Element('choose')
@ -526,7 +487,7 @@ class XmlSerializer(Serializer):
def deserialize_multi_instance(self, wf_spec, elem, cls=None,
**kwargs):
if cls == None:
if cls is None:
cls = MultiInstance
#cls = MultiInstance(wf_spec,elem.find('name'),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