2022-10-12 10:19:53 -04:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import ast
|
|
|
|
import copy
|
|
|
|
import sys
|
|
|
|
import traceback
|
2023-02-02 20:59:28 -05:00
|
|
|
import warnings
|
2022-10-12 10:19:53 -04:00
|
|
|
|
2023-02-02 20:59:28 -05:00
|
|
|
from .PythonScriptEngineEnvironment import TaskDataEnvironment
|
2023-01-19 10:47:07 -05:00
|
|
|
from ..exceptions import SpiffWorkflowException, WorkflowTaskException
|
2022-10-12 10:19:53 -04:00
|
|
|
from ..operators import Operator
|
|
|
|
|
|
|
|
|
|
|
|
# Copyright (C) 2020 Kelly McDonald
|
|
|
|
#
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
class PythonScriptEngine(object):
|
|
|
|
"""
|
|
|
|
This should serve as a base for all scripting & expression evaluation
|
|
|
|
operations that are done within both BPMN and BMN. Eventually it will also
|
|
|
|
serve as a base for FEEL expressions as well
|
|
|
|
|
|
|
|
If you are uncomfortable with the use of eval() and exec, then you should
|
|
|
|
provide a specialised subclass that parses and executes the scripts /
|
|
|
|
expressions in a different way.
|
|
|
|
"""
|
|
|
|
|
2023-02-02 20:59:28 -05:00
|
|
|
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',
|
|
|
|
DeprecationWarning, stacklevel=2)
|
|
|
|
if environment is None:
|
|
|
|
environment_globals = {}
|
|
|
|
environment_globals.update(default_globals or {})
|
|
|
|
environment_globals.update(scripting_additions or {})
|
|
|
|
self.environment = TaskDataEnvironment(environment_globals)
|
|
|
|
else:
|
|
|
|
self.environment = environment
|
2022-10-12 10:19:53 -04:00
|
|
|
self.error_tasks = {}
|
|
|
|
|
|
|
|
def validate(self, expression):
|
|
|
|
ast.parse(expression)
|
|
|
|
|
|
|
|
def evaluate(self, task, expression, external_methods=None):
|
|
|
|
"""
|
|
|
|
Evaluate the given expression, within the context of the given task and
|
|
|
|
return the result.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if isinstance(expression, Operator):
|
|
|
|
# I am assuming that this takes care of some kind of XML
|
|
|
|
# expression judging from the contents of operators.py
|
|
|
|
return expression._matches(task)
|
|
|
|
else:
|
|
|
|
return self._evaluate(expression, task.data, external_methods)
|
2023-01-19 10:47:07 -05:00
|
|
|
except SpiffWorkflowException as se:
|
|
|
|
se.add_note(f"Error evaluating expression '{expression}'")
|
|
|
|
raise se
|
2022-10-12 10:19:53 -04:00
|
|
|
except Exception as e:
|
2023-01-19 10:47:07 -05:00
|
|
|
raise WorkflowTaskException(f"Error evaluating expression '{expression}'", task=task, exception=e)
|
2022-10-12 10:19:53 -04:00
|
|
|
|
|
|
|
def execute(self, task, script, external_methods=None):
|
|
|
|
"""
|
|
|
|
Execute the script, within the context of the specified task
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self.check_for_overwrite(task, external_methods or {})
|
|
|
|
self._execute(script, task.data, external_methods or {})
|
|
|
|
except Exception as err:
|
2022-10-26 16:24:47 -04:00
|
|
|
wte = self.create_task_exec_exception(task, script, err)
|
2022-10-12 10:19:53 -04:00
|
|
|
self.error_tasks[task.id] = wte
|
|
|
|
raise wte
|
|
|
|
|
|
|
|
def call_service(self, operation_name, operation_params, task_data):
|
|
|
|
"""Override to control how external services are called from service
|
|
|
|
tasks."""
|
|
|
|
raise NotImplementedError("To call external services override the script engine and implement `call_service`.")
|
|
|
|
|
2022-10-26 16:24:47 -04:00
|
|
|
def create_task_exec_exception(self, task, script, err):
|
2023-01-19 10:47:07 -05:00
|
|
|
line_number, error_line = self.get_error_line_number_and_content(script, err)
|
|
|
|
if isinstance(err, SpiffWorkflowException):
|
|
|
|
err.line_number = line_number
|
|
|
|
err.error_line = error_line
|
|
|
|
err.add_note(f"Python script error on line {line_number}: '{error_line}'")
|
2022-10-12 10:19:53 -04:00
|
|
|
return err
|
|
|
|
detail = err.__class__.__name__
|
|
|
|
if len(err.args) > 0:
|
|
|
|
detail += ":" + err.args[0]
|
2023-01-19 10:47:07 -05:00
|
|
|
return WorkflowTaskException(detail, task=task, exception=err, line_number=line_number, error_line=error_line)
|
|
|
|
|
|
|
|
def get_error_line_number_and_content(self, script, err):
|
2022-10-12 10:19:53 -04:00
|
|
|
line_number = 0
|
|
|
|
error_line = ''
|
2023-01-19 10:47:07 -05:00
|
|
|
if isinstance(err, SyntaxError):
|
|
|
|
line_number = err.lineno
|
|
|
|
else:
|
|
|
|
cl, exc, tb = sys.exc_info()
|
|
|
|
# Loop back through the stack trace to find the file called
|
|
|
|
# 'string' - which is the script we are executing, then use that
|
|
|
|
# to parse and pull out the offending line.
|
|
|
|
for frame_summary in traceback.extract_tb(tb):
|
|
|
|
if frame_summary.filename == '<string>':
|
|
|
|
line_number = frame_summary.lineno
|
|
|
|
if line_number > 0:
|
|
|
|
error_line = script.splitlines()[line_number - 1]
|
|
|
|
return line_number, error_line
|
2022-10-12 10:19:53 -04:00
|
|
|
|
|
|
|
def check_for_overwrite(self, task, external_methods):
|
|
|
|
"""It's possible that someone will define a variable with the
|
|
|
|
same name as a pre-defined script, rending the script un-callable.
|
|
|
|
This results in a nearly indecipherable error. Better to fail
|
|
|
|
fast with a sensible error message."""
|
2023-02-02 20:59:28 -05:00
|
|
|
func_overwrites = set(self.environment.globals).intersection(task.data)
|
2022-10-12 10:19:53 -04:00
|
|
|
func_overwrites.update(set(external_methods).intersection(task.data))
|
|
|
|
if len(func_overwrites) > 0:
|
|
|
|
msg = f"You have task data that overwrites a predefined " \
|
|
|
|
f"function(s). Please change the following variable or " \
|
|
|
|
f"field name(s) to something else: {func_overwrites}"
|
2023-01-19 10:47:07 -05:00
|
|
|
raise WorkflowTaskException(msg, task=task)
|
2022-10-12 10:19:53 -04:00
|
|
|
|
|
|
|
def _evaluate(self, expression, context, external_methods=None):
|
2023-02-02 20:59:28 -05:00
|
|
|
return self.environment.evaluate(expression, context, external_methods)
|
2022-10-12 10:19:53 -04:00
|
|
|
|
|
|
|
def _execute(self, script, context, external_methods=None):
|
2023-02-02 20:59:28 -05:00
|
|
|
self.environment.execute(script, context, external_methods)
|