diff --git a/src/spiffworkflow_backend/routes/process_api_blueprint.py b/src/spiffworkflow_backend/routes/process_api_blueprint.py index cbb74d38..23475906 100644 --- a/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -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) diff --git a/src/spiffworkflow_backend/scripts/fact_service.py b/src/spiffworkflow_backend/scripts/fact_service.py new file mode 100644 index 00000000..16f8b593 --- /dev/null +++ b/src/spiffworkflow_backend/scripts/fact_service.py @@ -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 diff --git a/src/spiffworkflow_backend/scripts/get_env.py b/src/spiffworkflow_backend/scripts/get_env.py new file mode 100644 index 00000000..7aff254f --- /dev/null +++ b/src/spiffworkflow_backend/scripts/get_env.py @@ -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 diff --git a/src/spiffworkflow_backend/scripts/script.py b/src/spiffworkflow_backend/scripts/script.py index d7a2c67a..16cc9b2c 100644 --- a/src/spiffworkflow_backend/scripts/script.py +++ b/src/spiffworkflow_backend/scripts/script.py @@ -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) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 2adc067d..8ec69e72 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -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: diff --git a/src/spiffworkflow_backend/services/script_unit_test_runner.py b/src/spiffworkflow_backend/services/script_unit_test_runner.py index b3a0d91d..b9756050 100644 --- a/src/spiffworkflow_backend/services/script_unit_test_runner.py +++ b/src/spiffworkflow_backend/services/script_unit_test_runner.py @@ -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 ) diff --git a/tests/data/simple_script/simple_script.bpmn b/tests/data/simple_script/simple_script.bpmn index 72191979..ab573ce4 100644 --- a/tests/data/simple_script/simple_script.bpmn +++ b/tests/data/simple_script/simple_script.bpmn @@ -1,5 +1,5 @@ - + Flow_1k9q28c @@ -15,7 +15,8 @@ Flow_1fviiob a = 1 b = 2 -c = a + b +c = a + b +norris=fact_service(type='norris') ## Display Data diff --git a/tests/spiffworkflow_backend/unit/test_environment_var_script.py b/tests/spiffworkflow_backend/unit/test_environment_var_script.py new file mode 100644 index 00000000..ac96e7e4 --- /dev/null +++ b/tests/spiffworkflow_backend/unit/test_environment_var_script.py @@ -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" diff --git a/tests/spiffworkflow_backend/unit/test_process_instance_processor.py b/tests/spiffworkflow_backend/unit/test_process_instance_processor.py index 02a8c53e..db307421 100644 --- a/tests/spiffworkflow_backend/unit/test_process_instance_processor.py +++ b/tests/spiffworkflow_backend/unit/test_process_instance_processor.py @@ -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." + ) diff --git a/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py b/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py index e60be2a2..69c54851 100644 --- a/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py +++ b/tests/spiffworkflow_backend/unit/test_script_unit_test_runner.py @@ -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