Squashed 'SpiffWorkflow/' changes from 5cdb881ed..a6392d190

a6392d190 SpiffWorkflow cold start improvements (#13)

git-subtree-dir: SpiffWorkflow
git-subtree-split: a6392d19061f623394f5705fb78af23673d3940d
This commit is contained in:
burnettk 2022-11-02 12:19:52 -04:00
parent 9bce943d81
commit f3b2b10d37
15 changed files with 79 additions and 62 deletions

View File

@ -277,14 +277,9 @@ We'll cover a simple extension of custom script engine here. There is also an e
a similar engine based on `RestrictedPython <https://restrictedpython.readthedocs.io/en/latest/>`_ a similar engine based on `RestrictedPython <https://restrictedpython.readthedocs.io/en/latest/>`_
included alongside this example. included alongside this example.
The default script engine imports the following objects: The default script engine does not import any objects.
- :code:`timedelta` You could add functions or classes from the standard python modules or any code you've
- :code:`datetime`
- :code:`dateparser`
- :code:`pytz`
You could add other functions or classes from the standard python modules or any code you've
implemented yourself. Your global environment can be passed in using the `default_globals` implemented yourself. Your global environment can be passed in using the `default_globals`
argument when initializing the script engine. In our RestrictedPython example, we use their argument when initializing the script engine. In our RestrictedPython example, we use their
`safe_globals` which prevents users from executing some potentially unsafe operations. `safe_globals` which prevents users from executing some potentially unsafe operations.

View File

@ -3,6 +3,4 @@
celery==5.2.3 celery==5.2.3
coverage coverage
lxml lxml
dateparser
pytz
. .

View File

@ -22,7 +22,7 @@ setup(name='SpiffWorkflow',
license='lGPLv2', license='lGPLv2',
packages=find_packages(exclude=['tests', 'tests.*']), packages=find_packages(exclude=['tests', 'tests.*']),
package_data={'SpiffWorkflow.bpmn.parser.schema': ['*.xsd']}, package_data={'SpiffWorkflow.bpmn.parser.schema': ['*.xsd']},
install_requires=['configparser', 'lxml', 'celery', 'dateparser', 'pytz', install_requires=['configparser', 'lxml', 'celery',
# required for python 3.7 - https://stackoverflow.com/a/73932581 # required for python 3.7 - https://stackoverflow.com/a/73932581
'importlib-metadata<5.0; python_version <= "3.7"'], 'importlib-metadata<5.0; python_version <= "3.7"'],
keywords='spiff workflow bpmn engine', keywords='spiff workflow bpmn engine',

View File

@ -5,6 +5,7 @@ import datetime
import time import time
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym' __author__ = 'kellym'
@ -15,8 +16,9 @@ class NITimerDurationTest(BpmnWorkflowTestCase):
Non-Interrupting Timer boundary test Non-Interrupting Timer boundary test
""" """
def setUp(self): def setUp(self):
self.script_engine = PythonScriptEngine(default_globals={"timedelta": datetime.timedelta})
spec, subprocesses = self.load_workflow_spec('timer-non-interrupt-boundary.bpmn', 'NonInterruptTimer') spec, subprocesses = self.load_workflow_spec('timer-non-interrupt-boundary.bpmn', 'NonInterruptTimer')
self.workflow = BpmnWorkflow(spec, subprocesses) self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=self.script_engine)
def load_spec(self): def load_spec(self):
return return
@ -47,7 +49,9 @@ class NITimerDurationTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
if len(ready_tasks) > 1: if len(ready_tasks) > 1:
break break
if save_restore: self.save_restore() if save_restore:
self.save_restore()
self.workflow.script_engine = self.script_engine
#self.assertEqual(1, len(self.workflow.get_tasks(Task.WAITING))) #self.assertEqual(1, len(self.workflow.get_tasks(Task.WAITING)))
time.sleep(0.1) time.sleep(0.1)
self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.complete_task_from_id(ready_tasks[0].id)

View File

@ -24,32 +24,11 @@ class PythonScriptEngineTest(BpmnWorkflowTestCase):
workflow.do_engine_steps() workflow.do_engine_steps()
self.task = workflow.last_task self.task = workflow.last_task
def testDateTimeExpressions(self):
"""Basically, assure that we can use datime, dateutils, and pytz"""
script = """
# Create Current Date as UTC
now_utc = datetime.datetime.now(datetime.timezone.utc)
# Create Current Date at EST
now_est = now_utc.astimezone(pytz.timezone('US/Eastern'))
# Format a date from a date String in UTC
datestr = "2021-09-23 16:11:00 -0000" # 12 pm EST, 4pm UTC
dt = dateparser.parse(datestr)
localtime = dt.astimezone(pytz.timezone('US/Eastern'))
localtime_str = localtime.strftime("%Y-%m-%d %H:%M:%S")
"""
self.expressionEngine.execute(self.task, script)
self.assertEqual(self.task.data['now_utc'].utcoffset().days, 0)
self.assertEqual(self.task.data['now_est'].tzinfo.zone, "US/Eastern")
self.assertEqual(self.task.data['localtime_str'], "2021-09-23 12:11:00")
self.assertTrue(True)
def testFunctionsAndGlobalsAreRemoved(self): def testFunctionsAndGlobalsAreRemoved(self):
self.assertIn('testvar', self.task.data) self.assertIn('testvar', self.task.data)
self.assertIn('testvar2', self.task.data) self.assertIn('testvar2', self.task.data)
self.assertIn('sample', self.task.data) self.assertIn('sample', self.task.data)
self.assertNotIn('my_function', self.task.data) self.assertNotIn('my_function', self.task.data)
self.assertNotIn('datetime', self.task.data)
def suite(): def suite():
return unittest.TestLoader().loadTestsFromTestCase(PythonScriptEngineTest) return unittest.TestLoader().loadTestsFromTestCase(PythonScriptEngineTest)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import unittest import unittest
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
@ -8,6 +9,15 @@ from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'sartography' __author__ = 'sartography'
class CustomScriptEngine(PythonScriptEngine):
"""This is a custom script processor that can be easily injected into Spiff Workflow.
It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """
def __init__(self):
augment_methods = {
'timedelta': datetime.timedelta,
}
super().__init__(scripting_additions=augment_methods)
class TooManyLoopsTest(BpmnWorkflowTestCase): class TooManyLoopsTest(BpmnWorkflowTestCase):
@ -23,7 +33,7 @@ class TooManyLoopsTest(BpmnWorkflowTestCase):
def actual_test(self,save_restore = False): def actual_test(self,save_restore = False):
spec, subprocesses = self.load_workflow_spec('too_many_loops*.bpmn', 'loops') spec, subprocesses = self.load_workflow_spec('too_many_loops*.bpmn', 'loops')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=PythonScriptEngine()) self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
counter = 0 counter = 0
data = {} data = {}
while not self.workflow.is_completed(): while not self.workflow.is_completed():
@ -34,6 +44,7 @@ class TooManyLoopsTest(BpmnWorkflowTestCase):
counter += 1 # There is a 10 millisecond wait task. counter += 1 # There is a 10 millisecond wait task.
if save_restore: if save_restore:
self.save_restore() self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
self.assertEqual(20, self.workflow.last_task.data['counter']) self.assertEqual(20, self.workflow.last_task.data['counter'])
def test_with_sub_process(self): def test_with_sub_process(self):
@ -41,7 +52,7 @@ class TooManyLoopsTest(BpmnWorkflowTestCase):
# right after a sub-process. So assuring this is fixed. # right after a sub-process. So assuring this is fixed.
counter = 0 counter = 0
spec, subprocesses = self.load_workflow_spec('too_many_loops_sub_process.bpmn', 'loops_sub') spec, subprocesses = self.load_workflow_spec('too_many_loops_sub_process.bpmn', 'loops_sub')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=PythonScriptEngine()) self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
data = {} data = {}
while not self.workflow.is_completed(): while not self.workflow.is_completed():
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
@ -57,7 +68,7 @@ class TooManyLoopsTest(BpmnWorkflowTestCase):
def test_with_two_call_activities(self): def test_with_two_call_activities(self):
spec, subprocess = self.load_workflow_spec('sub_in_loop*.bpmn', 'main') spec, subprocess = self.load_workflow_spec('sub_in_loop*.bpmn', 'main')
self.workflow = BpmnWorkflow(spec, subprocess) self.workflow = BpmnWorkflow(spec, subprocess, script_engine=CustomScriptEngine())
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
for loop in range(3): for loop in range(3):
ready = self.workflow.get_ready_user_tasks() ready = self.workflow.get_ready_user_tasks()
@ -66,6 +77,7 @@ class TooManyLoopsTest(BpmnWorkflowTestCase):
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
def suite(): def suite():
return unittest.TestLoader().loadTestsFromTestCase(TooManyLoopsTest) return unittest.TestLoader().loadTestsFromTestCase(TooManyLoopsTest)

View File

@ -7,8 +7,8 @@
<bpmn:scriptTask id="Activity_1q1wged" name="Set Future Date"> <bpmn:scriptTask id="Activity_1q1wged" name="Set Future Date">
<bpmn:incoming>Flow_1i73q45</bpmn:incoming> <bpmn:incoming>Flow_1i73q45</bpmn:incoming>
<bpmn:outgoing>Flow_00e79cz</bpmn:outgoing> <bpmn:outgoing>Flow_00e79cz</bpmn:outgoing>
<bpmn:script>futuredate = dateparser.parse('in 1 second') - timedelta(seconds=.95) <bpmn:script>futuredate = datetime.now() + timedelta(0, 1) - timedelta(seconds=.95)
futuredate2 = dateparser.parse('September 1 2021 at 10am EDT')</bpmn:script> futuredate2 = datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M')</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1i73q45" sourceRef="Event_0u1rmur" targetRef="Activity_1q1wged" /> <bpmn:sequenceFlow id="Flow_1i73q45" sourceRef="Event_0u1rmur" targetRef="Activity_1q1wged" />
<bpmn:sequenceFlow id="Flow_00e79cz" sourceRef="Activity_1q1wged" targetRef="Event_0eb0w95" /> <bpmn:sequenceFlow id="Flow_00e79cz" sourceRef="Activity_1q1wged" targetRef="Event_0eb0w95" />

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import unittest import unittest
import time import time
@ -21,7 +22,10 @@ class CustomScriptEngine(PythonScriptEngine):
It will execute python code read in from the bpmn. It will also make any scripts in the It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """ scripts directory available for execution. """
def __init__(self): def __init__(self):
augment_methods = {'custom_function': my_custom_function} augment_methods = {
'custom_function': my_custom_function,
'timedelta': datetime.timedelta,
}
super().__init__(scripting_additions=augment_methods) super().__init__(scripting_additions=augment_methods)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
import unittest import unittest
import time import time
@ -21,7 +22,10 @@ class CustomScriptEngine(PythonScriptEngine):
It will execute python code read in from the bpmn. It will also make any scripts in the It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """ scripts directory available for execution. """
def __init__(self): def __init__(self):
augment_methods = {'custom_function': my_custom_function} augment_methods = {
'custom_function': my_custom_function,
'timedelta': datetime.timedelta,
}
super().__init__(scripting_additions=augment_methods) super().__init__(scripting_additions=augment_methods)

View File

@ -3,10 +3,10 @@
import unittest import unittest
import datetime import datetime
import time import time
import pytz
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym' __author__ = 'kellym'
@ -15,8 +15,12 @@ __author__ = 'kellym'
class TimerDateTest(BpmnWorkflowTestCase): class TimerDateTest(BpmnWorkflowTestCase):
def setUp(self): def setUp(self):
self.script_engine = PythonScriptEngine(default_globals={
"datetime": datetime.datetime,
"timedelta": datetime.timedelta,
})
self.spec, self.subprocesses = self.load_workflow_spec('timer-date-start.bpmn', 'date_timer') self.spec, self.subprocesses = self.load_workflow_spec('timer-date-start.bpmn', 'date_timer')
self.workflow = BpmnWorkflow(self.spec, self.subprocesses) self.workflow = BpmnWorkflow(self.spec, self.subprocesses, script_engine=self.script_engine)
def testRunThroughHappy(self): def testRunThroughHappy(self):
self.actual_test(save_restore=False) self.actual_test(save_restore=False)
@ -42,6 +46,7 @@ class TimerDateTest(BpmnWorkflowTestCase):
break break
if save_restore: if save_restore:
self.save_restore() self.save_restore()
self.workflow.script_engine = self.script_engine
waiting_tasks = self.workflow.get_tasks(TaskState.WAITING) waiting_tasks = self.workflow.get_tasks(TaskState.WAITING)
@ -50,8 +55,7 @@ class TimerDateTest(BpmnWorkflowTestCase):
loopcount = loopcount +1 loopcount = loopcount +1
endtime = datetime.datetime.now() endtime = datetime.datetime.now()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
tz = pytz.timezone('US/Eastern') testdate = datetime.datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M')
testdate = tz.localize(datetime.datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M'))
self.assertEqual(self.workflow.last_task.data['futuredate2'],testdate) self.assertEqual(self.workflow.last_task.data['futuredate2'],testdate)
self.assertTrue('completed' in self.workflow.last_task.data) self.assertTrue('completed' in self.workflow.last_task.data)
self.assertTrue(self.workflow.last_task.data['completed']) self.assertTrue(self.workflow.last_task.data['completed'])

View File

@ -3,10 +3,12 @@
import unittest import unittest
import datetime import datetime
import time import time
from datetime import timedelta
from SpiffWorkflow.bpmn.specs.events import EndEvent from SpiffWorkflow.bpmn.specs.events import EndEvent
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym' __author__ = 'kellym'
@ -14,8 +16,9 @@ __author__ = 'kellym'
class TimerDurationTest(BpmnWorkflowTestCase): class TimerDurationTest(BpmnWorkflowTestCase):
def setUp(self): def setUp(self):
self.script_engine = PythonScriptEngine(default_globals={"timedelta": timedelta})
self.spec, self.subprocesses = self.load_workflow_spec('boundary_timer_on_task.bpmn', 'test_timer') self.spec, self.subprocesses = self.load_workflow_spec('boundary_timer_on_task.bpmn', 'test_timer')
self.workflow = BpmnWorkflow(self.spec, self.subprocesses) self.workflow = BpmnWorkflow(self.spec, self.subprocesses, script_engine=self.script_engine)
def testRunThroughHappy(self): def testRunThroughHappy(self):
self.actual_test(save_restore=False) self.actual_test(save_restore=False)
@ -43,9 +46,11 @@ class TimerDurationTest(BpmnWorkflowTestCase):
starttime = datetime.datetime.now() starttime = datetime.datetime.now()
self.workflow = BpmnWorkflow(self.spec) self.workflow = BpmnWorkflow(self.spec)
self.workflow.script_engine = self.script_engine
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
if save_restore: if save_restore:
self.save_restore() self.save_restore()
self.workflow.script_engine = self.script_engine
time.sleep(0.1) time.sleep(0.1)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()

View File

@ -3,8 +3,10 @@
import unittest import unittest
import datetime import datetime
import time import time
from datetime import timedelta
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym' __author__ = 'kellym'
@ -13,8 +15,9 @@ __author__ = 'kellym'
class TimerDurationTest(BpmnWorkflowTestCase): class TimerDurationTest(BpmnWorkflowTestCase):
def setUp(self): def setUp(self):
self.script_engine = PythonScriptEngine(default_globals={"timedelta": timedelta})
self.spec, self.subprocesses = self.load_workflow_spec('timer.bpmn', 'timer') self.spec, self.subprocesses = self.load_workflow_spec('timer.bpmn', 'timer')
self.workflow = BpmnWorkflow(self.spec, self.subprocesses) self.workflow = BpmnWorkflow(self.spec, self.subprocesses, script_engine=self.script_engine)
def testRunThroughHappy(self): def testRunThroughHappy(self):
self.actual_test(save_restore=False) self.actual_test(save_restore=False)
@ -40,7 +43,9 @@ class TimerDurationTest(BpmnWorkflowTestCase):
while loopcount < 10: while loopcount < 10:
if len(self.workflow.get_tasks(TaskState.READY)) >= 1: if len(self.workflow.get_tasks(TaskState.READY)) >= 1:
break break
if save_restore: self.save_restore() if save_restore:
self.save_restore()
self.workflow.script_engine = self.script_engine
self.assertEqual(1, len(self.workflow.get_tasks(TaskState.WAITING))) self.assertEqual(1, len(self.workflow.get_tasks(TaskState.WAITING)))
time.sleep(0.1) time.sleep(0.1)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()

View File

@ -3,8 +3,10 @@
import unittest import unittest
import time import time
from datetime import timedelta
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from .BaseTestCase import BaseTestCase from .BaseTestCase import BaseTestCase
__author__ = 'kellym' __author__ = 'kellym'
@ -13,8 +15,9 @@ __author__ = 'kellym'
class MessageBoundaryTest(BaseTestCase): class MessageBoundaryTest(BaseTestCase):
def setUp(self): def setUp(self):
self.script_engine = PythonScriptEngine(default_globals={"timedelta": timedelta})
self.spec, self.subprocesses = self.load_workflow_spec('MessageBoundary.bpmn', 'Process_1kjyavs') self.spec, self.subprocesses = self.load_workflow_spec('MessageBoundary.bpmn', 'Process_1kjyavs')
self.workflow = BpmnWorkflow(self.spec, self.subprocesses) self.workflow = BpmnWorkflow(self.spec, self.subprocesses, script_engine=self.script_engine)
def testRunThroughHappy(self): def testRunThroughHappy(self):
self.actual_test(save_restore=False) self.actual_test(save_restore=False)
@ -41,7 +44,9 @@ class MessageBoundaryTest(BaseTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
time.sleep(.01) time.sleep(.01)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
if save_restore: self.save_restore() if save_restore:
self.save_restore()
self.workflow.script_engine = self.script_engine
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
time.sleep(.01) time.sleep(.01)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()

View File

@ -1,3 +1,4 @@
import datetime
from decimal import Decimal from decimal import Decimal
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
@ -7,4 +8,5 @@ from ..DecisionRunner import DecisionRunner
class PythonDecisionRunner(DecisionRunner): class PythonDecisionRunner(DecisionRunner):
def __init__(self, filename): def __init__(self, filename):
super().__init__(PythonScriptEngine(scripting_additions={'Decimal': Decimal}), filename, 'python_engine') scripting_additions={'Decimal': Decimal, 'datetime': datetime}
super().__init__(PythonScriptEngine(scripting_additions=scripting_additions), filename, 'python_engine')