From 6fab0f3d8abac5c39929d26c0a536e2cb8029f28 Mon Sep 17 00:00:00 2001 From: Valentin Zickner Date: Mon, 23 Sep 2024 17:42:54 +0200 Subject: [PATCH] add support for .robot files --- robocorp/flowable/robocorp_client/API.py | 26 +++++++++ robocorp/flowable/robocorp_client/__main__.py | 8 ++- .../flowable/robocorp_client/call_robocorp.py | 7 ++- .../robocorp_client/robocorp_handler.py | 55 ++++++++++++++++++- 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 robocorp/flowable/robocorp_client/API.py diff --git a/robocorp/flowable/robocorp_client/API.py b/robocorp/flowable/robocorp_client/API.py new file mode 100644 index 0000000..b648015 --- /dev/null +++ b/robocorp/flowable/robocorp_client/API.py @@ -0,0 +1,26 @@ +import json +import os + +from robot.api.deco import library, keyword + + +@library +class API: + + result = {} + + @keyword + def flw_input(self, name: str): + variables = os.environ["FLOWABLE_INPUT_VARIABLES"] + obj = json.loads(variables) + return obj[name] + + @keyword + def flw_output(self, name: str, value: any): + output_file_name = os.environ.get("FLOWABLE_OUTPUT_FILE") + self.result[name] = value + print('assign ' + name + '=' + value) + if output_file_name is not None: + with open(output_file_name, "w") as f: + f.write(json.dumps(self.result)) + f.close() diff --git a/robocorp/flowable/robocorp_client/__main__.py b/robocorp/flowable/robocorp_client/__main__.py index 41aa158..82fa7a6 100644 --- a/robocorp/flowable/robocorp_client/__main__.py +++ b/robocorp/flowable/robocorp_client/__main__.py @@ -4,13 +4,13 @@ from flowable.external_worker_client import ExternalWorkerClient from flowable.external_worker_client.cloud_token import FlowableCloudToken -from flowable.robocorp_client.robocorp_handler import RobocorpActionHandler, RobocorpTaskHandler +from flowable.robocorp_client.robocorp_handler import RobocorpActionHandler, RobocorpTaskHandler, RobocorpRobotHandler if __name__ == "__main__": parser = argparse.ArgumentParser(description="Flowable Robocorp Client") parser.add_argument('topic', help='Topic of the Robocorp Action to listen to') - parser.add_argument('mode', type=str, choices=['action', 'task'], help='Type of robocorp action/task') + parser.add_argument('mode', type=str, choices=['action', 'task', 'robot'], help='Type of robocorp action/task/robot') parser.add_argument('path', type=str, help='The directory or file with the actions/tasks to run.') parser.add_argument('--flowable-host', type=str, default='https://trial.flowable.com', help='URL of Flowable Work') parser.add_argument('--flowable-token', type=str, help='Bearer Token, can be used for example with the Flowable Trial') @@ -32,6 +32,8 @@ if args.mode == 'action': robocorp_job_handler = RobocorpActionHandler(robocorp_action_file) - else: + elif args.mode == 'task': robocorp_job_handler = RobocorpTaskHandler(robocorp_action_file) + else: + robocorp_job_handler = RobocorpRobotHandler(robocorp_action_file) subscription = client.subscribe(topic, robocorp_job_handler.handle_task) diff --git a/robocorp/flowable/robocorp_client/call_robocorp.py b/robocorp/flowable/robocorp_client/call_robocorp.py index 39a5000..20a997e 100644 --- a/robocorp/flowable/robocorp_client/call_robocorp.py +++ b/robocorp/flowable/robocorp_client/call_robocorp.py @@ -1,12 +1,15 @@ +import os import subprocess import sys -def call_robocorp(args, mod_name='robocorp.actions'): +def call_robocorp(args, mod_name, env=None): + if env is None: + env = os.environ.copy() call_args = [sys.executable, "-m", mod_name] call_args.extend(args) try: - return subprocess.run(call_args, capture_output=True, text=True) + return subprocess.run(call_args, capture_output=True, text=True, env=env) except subprocess.CalledProcessError as exc: print("ERROR: failed to call robocorp", exc.returncode, exc.output) diff --git a/robocorp/flowable/robocorp_client/robocorp_handler.py b/robocorp/flowable/robocorp_client/robocorp_handler.py index 1102b16..be0bffa 100644 --- a/robocorp/flowable/robocorp_client/robocorp_handler.py +++ b/robocorp/flowable/robocorp_client/robocorp_handler.py @@ -1,6 +1,8 @@ import ast import json +import os import shlex +import tempfile from flowable.external_worker_client import ExternalWorkerAcquireJobResponse, WorkerResultBuilder from flowable.robocorp_client.call_robocorp import call_robocorp @@ -18,6 +20,18 @@ def extract_action_and_parameters(job): return action, params +def extract_action_and_parameters_map(job): + action = None + params = {} + for variable in job.variables: + if variable.name == '__robocorpTaskName': + action = variable.value + else: + if variable.value is not None: + params[variable.name] = variable.value + return action, params + + def extract_result(results): result_item = None lines = results.strip().splitlines() @@ -35,8 +49,7 @@ def create_output_dir(job): return 'output/' + job.id + '-' + (job.scope_id or job.process_instance_id) + '-' + job.element_id -def add_variables_to_result(work_result, result): - json_result = ast.literal_eval(result) +def add_variables_to_result(work_result, json_result): if isinstance(json_result, dict): for key in json_result: @@ -78,7 +91,8 @@ def handle_task(self, job: ExternalWorkerAcquireJobResponse, worker_result_build result = extract_result(results.stdout) print('---> Job execution done for "' + job.id + '" with result ' + result + '". Output saved to ' + output_dir, robocorp_args) work_result = worker_result_builder.success() - return add_variables_to_result(work_result, result) + json_result = ast.literal_eval(result) + return add_variables_to_result(work_result, json_result) class RobocorpTaskHandler: @@ -100,3 +114,38 @@ def handle_task(self, job: ExternalWorkerAcquireJobResponse, worker_result_build return worker_result_builder.failure().error_message('failed with status code ' + str(results.returncode)).error_details(results.stderr + results.stdout) print('---> Job execution done for "' + job.id + '". Output saved to ' + output_dir, robocorp_args) return worker_result_builder.success() + + +class RobocorpRobotHandler: + def __init__(self, robocorp_action_file: str): + self.robocorp_action_file = robocorp_action_file + + def handle_task(self, job: ExternalWorkerAcquireJobResponse, worker_result_builder: WorkerResultBuilder): + action, params = extract_action_and_parameters_map(job) + if action is None: + return worker_result_builder.failure().error_message('failed to find robocorp action name') + + output_dir = create_output_dir(job) + robocorp_args = ['--rpa', '--name', action.__str__(), '--report', 'NONE', '--outputdir', output_dir, self.robocorp_action_file] + print('---> Execute job "' + job.id + '"', robocorp_args) + tmp = tempfile.NamedTemporaryFile(delete=False) + tmp.close() + new_env = os.environ.copy() + new_env["FLOWABLE_INPUT_VARIABLES"] = json.dumps(params) + new_env["FLOWABLE_OUTPUT_FILE"] = tmp.name + results = call_robocorp(robocorp_args, mod_name='robot', env=new_env) + + if results.returncode != 0: + print('---> Job execution failed for "' + job.id + '". Output saved to ' + output_dir) + os.unlink(tmp.name) + return worker_result_builder.failure().error_message('failed with status code ' + str(results.returncode)).error_details(results.stderr + results.stdout) + print('---> Job execution done for "' + job.id + '". Output saved to ' + output_dir, robocorp_args) + + with open(tmp.name, 'r') as content_file: + content = json.loads(content_file.read()) + os.unlink(tmp.name) + + if content is None: + return worker_result_builder.success() + else: + return add_variables_to_result(worker_result_builder.success(), content)