Merge remote-tracking branch 'origin/main' into feature/script-unit-test-subprocesses
This commit is contained in:
commit
c567cae994
|
@ -1087,7 +1087,7 @@ def script_unit_test_run(
|
|||
input_json = get_required_parameter_or_raise("input_json", body)
|
||||
expected_output_json = get_required_parameter_or_raise("expected_output_json", body)
|
||||
|
||||
result = ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
|
||||
result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
|
||||
python_script, input_json, expected_output_json
|
||||
)
|
||||
return make_response(jsonify(result), 200)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
"""Fact_service."""
|
||||
from typing import Any
|
||||
|
||||
from SpiffWorkflow import Task as SpiffTask # type: ignore
|
||||
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
|
||||
|
||||
class FactService(Script):
|
||||
"""FactService."""
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""Get_description."""
|
||||
return """Just your basic class that can pull in data from a few api endpoints and
|
||||
do a basic task."""
|
||||
|
||||
def run(
|
||||
self, task: SpiffTask, environment_identifier: str, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run."""
|
||||
if "type" not in kwargs:
|
||||
raise Exception("Please specify a 'type' of fact as a keyword argument.")
|
||||
else:
|
||||
fact = kwargs["type"]
|
||||
|
||||
if fact == "cat":
|
||||
details = "The cat in the hat" # self.get_cat()
|
||||
elif fact == "norris":
|
||||
details = "Chuck Norris doesn’t read books. He stares them down until he gets the information he wants."
|
||||
elif fact == "buzzword":
|
||||
details = "Move the Needle." # self.get_buzzword()
|
||||
else:
|
||||
details = "unknown fact type."
|
||||
|
||||
# self.add_data_to_task(task, details)
|
||||
return details
|
|
@ -0,0 +1,20 @@
|
|||
"""Get_env."""
|
||||
from typing import Any
|
||||
|
||||
from SpiffWorkflow import Task as SpiffTask # type: ignore
|
||||
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
|
||||
|
||||
class GetEnv(Script):
|
||||
"""GetEnv."""
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""Get_description."""
|
||||
return """Returns the current environment - ie testing, staging, production."""
|
||||
|
||||
def run(
|
||||
self, task: SpiffTask, environment_identifier: str, *_args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Run."""
|
||||
return environment_identifier
|
|
@ -9,9 +9,7 @@ from typing import Any
|
|||
from typing import Callable
|
||||
|
||||
from flask_bpmn.api.api_error import ApiError
|
||||
|
||||
from spiffworkflow_backend.models.task import Task
|
||||
|
||||
from SpiffWorkflow import Task as SpiffTask # type: ignore
|
||||
|
||||
# Generally speaking, having some global in a flask app is TERRIBLE.
|
||||
# This is here, because after loading the application this will never change under
|
||||
|
@ -22,76 +20,30 @@ SCRIPT_SUB_CLASSES = None
|
|||
class Script:
|
||||
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
|
||||
|
||||
def get_description(self) -> None:
|
||||
@abstractmethod
|
||||
def get_description(self) -> str:
|
||||
"""Get_description."""
|
||||
raise ApiError("invalid_script", "This script does not supply a description.")
|
||||
|
||||
@abstractmethod
|
||||
def do_task(
|
||||
self, task: Task, workflow_id: int, *args: list[Any], **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
"""Do_task."""
|
||||
def run(
|
||||
self,
|
||||
task: SpiffTask,
|
||||
environment_identifier: str,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Run."""
|
||||
raise ApiError(
|
||||
"invalid_script",
|
||||
"This is an internal error. The script you are trying to execute '%s' "
|
||||
% self.__class__.__name__
|
||||
+ "does not properly implement the do_task function.",
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def do_task_validate_only(
|
||||
self, task: Task, workflow_id: int, *args: list[Any], **kwargs: dict[Any, Any]
|
||||
) -> None:
|
||||
"""Do_task_validate_only."""
|
||||
raise ApiError(
|
||||
"invalid_script",
|
||||
"This is an internal error. The script you are trying to execute '%s' "
|
||||
% self.__class__.__name__
|
||||
+ "does must provide a validate_only option that mimics the do_task, "
|
||||
+ "but does not make external calls or database updates.",
|
||||
+ "does not properly implement the run function.",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def generate_augmented_list(task: Task, workflow_id: int) -> dict[str, Callable]:
|
||||
"""This makes a dictionary of lambda functions that are closed over the class instance that they represent.
|
||||
|
||||
This is passed into PythonScriptParser as a list of helper functions that are
|
||||
available for running. In general, they maintain the do_task call structure that they had, but
|
||||
they always return a value rather than updating the task data.
|
||||
|
||||
We may be able to remove the task for each of these calls if we are not using it other than potentially
|
||||
updating the task data.
|
||||
"""
|
||||
|
||||
def make_closure(
|
||||
subclass: type[Script], task: Task, workflow_id: int
|
||||
) -> Callable:
|
||||
"""Yes - this is black magic.
|
||||
|
||||
Essentially, we want to build a list of all of the submodules (i.e. email, user_data_get, etc)
|
||||
and a function that is assocated with them.
|
||||
This basically creates an Instance of the class and returns a function that calls do_task
|
||||
on the instance of that class.
|
||||
the next for x in range, then grabs the name of the module and associates it with the function
|
||||
that we created.
|
||||
"""
|
||||
instance = subclass()
|
||||
return lambda *ar, **kw: subclass.do_task(
|
||||
instance, task, workflow_id, *ar, **kw
|
||||
)
|
||||
|
||||
execlist = {}
|
||||
subclasses = Script.get_all_subclasses()
|
||||
for x in range(len(subclasses)):
|
||||
subclass = subclasses[x]
|
||||
execlist[subclass.__module__.split(".")[-1]] = make_closure(
|
||||
subclass, task, workflow_id
|
||||
)
|
||||
return execlist
|
||||
|
||||
@staticmethod
|
||||
def generate_augmented_validate_list(
|
||||
task: Task, workflow_id: int
|
||||
def generate_augmented_list(
|
||||
task: SpiffTask, environment_identifier: str
|
||||
) -> dict[str, Callable]:
|
||||
"""This makes a dictionary of lambda functions that are closed over the class instance that they represent.
|
||||
|
||||
|
@ -103,21 +55,33 @@ class Script:
|
|||
updating the task data.
|
||||
"""
|
||||
|
||||
def make_closure_validate(
|
||||
subclass: type[Script], task: Task, workflow_id: int
|
||||
def make_closure(
|
||||
subclass: type[Script], task: SpiffTask, environment_identifier: str
|
||||
) -> Callable:
|
||||
"""Make_closure_validate."""
|
||||
"""Yes - this is black magic.
|
||||
|
||||
Essentially, we want to build a list of all of the submodules (i.e. email, user_data_get, etc)
|
||||
and a function that is assocated with them.
|
||||
This basically creates an Instance of the class and returns a function that calls do_task
|
||||
on the instance of that class.
|
||||
the next for x in range, then grabs the name of the module and associates it with the function
|
||||
that we created.
|
||||
"""
|
||||
instance = subclass()
|
||||
return lambda *a, **b: subclass.do_task_validate_only(
|
||||
instance, task, workflow_id, *a, **b
|
||||
return lambda *ar, **kw: subclass.run(
|
||||
instance,
|
||||
task,
|
||||
environment_identifier,
|
||||
*ar,
|
||||
**kw,
|
||||
)
|
||||
|
||||
execlist = {}
|
||||
subclasses = Script.get_all_subclasses()
|
||||
for x in range(len(subclasses)):
|
||||
subclass = subclasses[x]
|
||||
execlist[subclass.__module__.split(".")[-1]] = make_closure_validate(
|
||||
subclass, task, workflow_id
|
||||
execlist[subclass.__module__.split(".")[-1]] = make_closure(
|
||||
subclass, task=task, environment_identifier=environment_identifier
|
||||
)
|
||||
return execlist
|
||||
|
||||
|
@ -146,29 +110,3 @@ class Script:
|
|||
all_subclasses.extend(Script._get_all_subclasses(subclass))
|
||||
|
||||
return all_subclasses
|
||||
|
||||
def add_data_to_task(self, task: Task, data: Any) -> None:
|
||||
"""Add_data_to_task."""
|
||||
key = self.__class__.__name__
|
||||
|
||||
if task.data is None:
|
||||
task.data = {}
|
||||
|
||||
if key in task.data:
|
||||
task.data[key].update(data)
|
||||
else:
|
||||
task.data[key] = data
|
||||
|
||||
|
||||
class ScriptValidationError:
|
||||
"""ScriptValidationError."""
|
||||
|
||||
def __init__(self, code: str, message: str):
|
||||
"""__init__."""
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
@classmethod
|
||||
def from_api_error(cls, api_error: ApiError) -> ScriptValidationError:
|
||||
"""From_api_error."""
|
||||
return cls(api_error.code, api_error.message)
|
||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
|||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import NewType
|
||||
|
@ -68,6 +69,7 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfo
|
|||
from spiffworkflow_backend.models.task_event import TaskAction
|
||||
from spiffworkflow_backend.models.task_event import TaskEventModel
|
||||
from spiffworkflow_backend.models.user import UserModelSchema
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.process_model_service import ProcessModelService
|
||||
from spiffworkflow_backend.services.service_task_service import ServiceTaskService
|
||||
|
@ -86,6 +88,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
|||
scripts directory available for execution.
|
||||
"""
|
||||
|
||||
def __get_augment_methods(self, task: SpiffTask) -> Dict[str, Callable]:
|
||||
"""__get_augment_methods."""
|
||||
return Script.generate_augmented_list(task, current_app.env)
|
||||
|
||||
def evaluate(self, task: SpiffTask, expression: str) -> Any:
|
||||
"""Evaluate."""
|
||||
return self._evaluate(expression, task.data, task)
|
||||
|
@ -97,11 +103,14 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
|||
task: Optional[SpiffTask] = None,
|
||||
external_methods: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
"""_evaluate."""
|
||||
methods = self.__get_augment_methods(task)
|
||||
if external_methods:
|
||||
methods.update(external_methods)
|
||||
|
||||
"""Evaluate the given expression, within the context of the given task and return the result."""
|
||||
try:
|
||||
return super()._evaluate(
|
||||
expression, context, external_methods=external_methods
|
||||
)
|
||||
return super()._evaluate(expression, context, external_methods=methods)
|
||||
except Exception as exception:
|
||||
if task is None:
|
||||
raise ProcessInstanceProcessorError(
|
||||
|
@ -120,7 +129,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
|
|||
) -> None:
|
||||
"""Execute."""
|
||||
try:
|
||||
super().execute(task, script, external_methods)
|
||||
methods = self.__get_augment_methods(task)
|
||||
if external_methods:
|
||||
methods.update(external_methods)
|
||||
super().execute(task, script, methods)
|
||||
except WorkflowException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
|
|
|
@ -33,7 +33,7 @@ class ScriptUnitTestRunner:
|
|||
_script_engine = CustomBpmnScriptEngine()
|
||||
|
||||
@classmethod
|
||||
def run_with_task_and_script_and_pre_post_contexts(
|
||||
def run_with_script_and_pre_post_contexts(
|
||||
cls,
|
||||
script: str,
|
||||
input_context: PythonScriptContext,
|
||||
|
@ -116,6 +116,6 @@ class ScriptUnitTestRunner:
|
|||
)
|
||||
|
||||
script = task.task_spec.script
|
||||
return cls.run_with_task_and_script_and_pre_post_contexts(
|
||||
return cls.run_with_script_and_pre_post_contexts(
|
||||
script, input_context, expected_output_context
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1kbzkan" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1kbzkan" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
|
||||
<bpmn:process id="Process_SimpleScript" name="Simple Script" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1k9q28c</bpmn:outgoing>
|
||||
|
@ -15,7 +15,8 @@
|
|||
<bpmn:outgoing>Flow_1fviiob</bpmn:outgoing>
|
||||
<bpmn:script>a = 1
|
||||
b = 2
|
||||
c = a + b</bpmn:script>
|
||||
c = a + b
|
||||
norris=fact_service(type='norris')</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:manualTask id="Activity_DisplayData" name="Display Data">
|
||||
<bpmn:documentation>## Display Data
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
"""Test_environment_var_script."""
|
||||
from flask import Flask
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
|
||||
from spiffworkflow_backend.services.process_instance_processor import (
|
||||
ProcessInstanceProcessor,
|
||||
)
|
||||
|
||||
|
||||
class TestEnvironmentVarScript(BaseTest):
|
||||
"""TestEnvironmentVarScript."""
|
||||
|
||||
# it's not totally obvious we want to keep this test/file
|
||||
def test_script_engine_can_use_custom_scripts(
|
||||
self,
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
"""Test_script_engine_takes_data_and_returns_expected_results."""
|
||||
with app.app_context():
|
||||
script_engine = ProcessInstanceProcessor._script_engine
|
||||
result = script_engine._evaluate("get_env()", {})
|
||||
assert result == "testing"
|
|
@ -21,3 +21,15 @@ class TestProcessInstanceProcessor(BaseTest):
|
|||
|
||||
result = script_engine._evaluate("a", {"a": 1})
|
||||
assert result == 1
|
||||
|
||||
def test_script_engine_can_use_custom_scripts(
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
"""Test_script_engine_takes_data_and_returns_expected_results."""
|
||||
script_engine = ProcessInstanceProcessor._script_engine
|
||||
result = script_engine._evaluate("fact_service(type='norris')", {})
|
||||
assert (
|
||||
result
|
||||
== "Chuck Norris doesn’t read books. He stares them down until he gets the information he wants."
|
||||
)
|
||||
|
|
|
@ -38,10 +38,8 @@ class TestScriptUnitTestRunner(BaseTest):
|
|||
expected_output_context: PythonScriptContext = {"a": 2}
|
||||
script = "a = 2"
|
||||
|
||||
unit_test_result = (
|
||||
ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
|
||||
script, input_context, expected_output_context
|
||||
)
|
||||
unit_test_result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
|
||||
script, input_context, expected_output_context
|
||||
)
|
||||
|
||||
assert unit_test_result.result
|
||||
|
@ -72,10 +70,8 @@ class TestScriptUnitTestRunner(BaseTest):
|
|||
expected_output_context: PythonScriptContext = {"a": 2, "b": 3}
|
||||
script = "a = 2"
|
||||
|
||||
unit_test_result = (
|
||||
ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
|
||||
script, input_context, expected_output_context
|
||||
)
|
||||
unit_test_result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
|
||||
script, input_context, expected_output_context
|
||||
)
|
||||
|
||||
assert unit_test_result.result is not True
|
||||
|
|
Loading…
Reference in New Issue