From fdb87835473f97038d3d42c37682dcd6323efc75 Mon Sep 17 00:00:00 2001 From: Valentin Zickner <3200232+vzickner@users.noreply.github.com> Date: Fri, 20 Sep 2024 22:06:05 +0200 Subject: [PATCH] add robocorp client --- .github/workflows/main.yml | 33 +- .github/workflows/release.yml | 31 +- .github/workflows/test-release.yml | 34 +- README.md | 66 +- external-worker/README.md | 67 ++ .../flowable}/__init__.py | 0 .../external_worker_client/__init__.py | 9 +- .../external_worker_client/cloud_token.py | 0 .../engine_rest_variable.py | 0 .../external_worker_acquire_job_response.py | 0 .../external_worker_job_response.py | 0 .../flowable_rest_exception.py | 0 .../external_worker_client/list_result.py | 0 .../request_converter.py | 0 .../response_converter.py | 0 .../external_worker_client/restclient.py | 1 + .../flowable}/external_worker_client/utils.py | 0 .../external_worker_client/worker_result.py | 0 setup.py => external-worker/setup.py | 14 +- {tests => external-worker/tests}/__init__.py | 0 .../tests}/basic_test.py | 0 .../tests}/bpmn_utils.py | 0 .../tests}/cmmn_utils.py | 0 .../fixtures/cases/externalWorkerCase.cmmn | 0 .../fixtures/cassettes/test_acquire_jobs.yml | 0 .../fixtures/cassettes/test_complete_job.yml | 0 .../fixtures/cassettes/test_consume.yml | 0 .../test_consume_with_bpmn_error.yml | 0 .../test_consume_with_cmmn_terminate.yml | 0 .../fixtures/cassettes/test_fail_job.yml | 0 .../fixtures/cassettes/test_get_job.yml | 0 ...ror_without_error_code_1_execute_first.yml | 0 ...or_without_error_code_2_execute_second.yml | 0 ...error_without_error_code_execute_first.yml | 0 .../test_job_with_cmmn_terminate.yml | 0 .../fixtures/cassettes/test_list_jobs.yml | 0 .../test_list_jobs_customize_session.yml | 0 .../cassettes/test_list_jobs_invalid_auth.yml | 0 .../test_subscribe_with_bpmn_error.yml | 69 +- .../processes/externalWorkerProcess.bpmn | 0 .../fixtures/processes/withBoundaryEvent.bpmn | 0 {tests => external-worker/tests}/test_init.py | 0 .../tests}/test_restclient.py | 0 {tests => external-worker/tests}/vcr.py | 0 robocorp/README.md | 74 +++ robocorp/docs/robocorpExample.bpmn | 62 ++ robocorp/docs/robocorpExample.png | Bin 0 -> 13437 bytes robocorp/flowable/.gitignore | 1 + robocorp/flowable/__init__.py | 0 robocorp/flowable/robocorp_client/__init__.py | 0 robocorp/flowable/robocorp_client/__main__.py | 37 ++ .../flowable/robocorp_client/call_robocorp.py | 12 + .../robocorp_client/robocorp_handler.py | 102 +++ robocorp/setup.py | 22 + robocorp/tests/__init__.py | 0 robocorp/tests/basic_test.py | 20 + robocorp/tests/bpmn_utils.py | 55 ++ .../cassettes/test_with_custom_action.yml | 618 ++++++++++++++++++ .../fixtures/processes/robocorpExample.bpmn | 62 ++ robocorp/tests/robocorp_action_weather.py | 8 + robocorp/tests/test_robocorp_client.py | 57 ++ robocorp/tests/vcr.py | 12 + 62 files changed, 1375 insertions(+), 91 deletions(-) create mode 100644 external-worker/README.md rename {flowable => external-worker/flowable}/__init__.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/__init__.py (91%) rename {flowable => external-worker/flowable}/external_worker_client/cloud_token.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/engine_rest_variable.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/external_worker_acquire_job_response.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/external_worker_job_response.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/flowable_rest_exception.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/list_result.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/request_converter.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/response_converter.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/restclient.py (99%) rename {flowable => external-worker/flowable}/external_worker_client/utils.py (100%) rename {flowable => external-worker/flowable}/external_worker_client/worker_result.py (100%) rename setup.py => external-worker/setup.py (75%) rename {tests => external-worker/tests}/__init__.py (100%) rename {tests => external-worker/tests}/basic_test.py (100%) rename {tests => external-worker/tests}/bpmn_utils.py (100%) rename {tests => external-worker/tests}/cmmn_utils.py (100%) rename {tests => external-worker/tests}/fixtures/cases/externalWorkerCase.cmmn (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_acquire_jobs.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_complete_job.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_consume.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_consume_with_bpmn_error.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_consume_with_cmmn_terminate.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_fail_job.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_get_job.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_1_execute_first.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_2_execute_second.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_execute_first.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_job_with_cmmn_terminate.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_list_jobs.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_list_jobs_customize_session.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_list_jobs_invalid_auth.yml (100%) rename {tests => external-worker/tests}/fixtures/cassettes/test_subscribe_with_bpmn_error.yml (95%) rename {tests => external-worker/tests}/fixtures/processes/externalWorkerProcess.bpmn (100%) rename {tests => external-worker/tests}/fixtures/processes/withBoundaryEvent.bpmn (100%) rename {tests => external-worker/tests}/test_init.py (100%) rename {tests => external-worker/tests}/test_restclient.py (100%) rename {tests => external-worker/tests}/vcr.py (100%) create mode 100644 robocorp/README.md create mode 100644 robocorp/docs/robocorpExample.bpmn create mode 100644 robocorp/docs/robocorpExample.png create mode 100644 robocorp/flowable/.gitignore create mode 100644 robocorp/flowable/__init__.py create mode 100644 robocorp/flowable/robocorp_client/__init__.py create mode 100644 robocorp/flowable/robocorp_client/__main__.py create mode 100644 robocorp/flowable/robocorp_client/call_robocorp.py create mode 100644 robocorp/flowable/robocorp_client/robocorp_handler.py create mode 100644 robocorp/setup.py create mode 100644 robocorp/tests/__init__.py create mode 100644 robocorp/tests/basic_test.py create mode 100644 robocorp/tests/bpmn_utils.py create mode 100644 robocorp/tests/fixtures/cassettes/test_with_custom_action.yml create mode 100644 robocorp/tests/fixtures/processes/robocorpExample.bpmn create mode 100644 robocorp/tests/robocorp_action_weather.py create mode 100644 robocorp/tests/test_robocorp_client.py create mode 100644 robocorp/tests/vcr.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71fe7c4..98187ed 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,31 @@ -name: External Worker Build and Test +name: Flowable External Client Build and Test on: [push, pull_request] jobs: - build: + test-external-worker-client: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12.0"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - run: pip install virtualenv + - run: virtualenv venv + - run: source venv/bin/activate + - run: pip install setuptools + - run: pip install -e ".[testing]" + working-directory: ./external-worker/ + - run: python -m unittest discover + working-directory: ./external-worker/ + test-robocorp-client: runs-on: ubuntu-latest strategy: matrix: @@ -22,4 +43,10 @@ jobs: - run: virtualenv venv - run: source venv/bin/activate - run: pip install setuptools - - run: python setup.py pytest + - run: python setup.py install + working-directory: ./external-worker/ + - run: pip install -e ".[testing]" + working-directory: ./robocorp/ + - run: cp -a external-worker/flowable/external_worker_client robocorp/flowable/ # we need to copy this module over, since it's only searching locally for modules + - run: python -m unittest discover + working-directory: ./robocorp/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a280cd2..0b04614 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,38 @@ jobs: - run: source venv/bin/activate - run: pip install setuptools - run: python setup.py sdist + working-directory: ./external-worker/ - run: pip wheel --no-deps . -w dist + working-directory: ./external-worker/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - packages-dir: dist/ + packages-dir: external-worker/dist/ + verbose: true + + build-robocorp-client: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12.0 + uses: actions/setup-python@v4 + with: + python-version: 3.12.0 + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - run: pip install virtualenv + - run: virtualenv venv + - run: source venv/bin/activate + - run: pip install setuptools + - run: python setup.py sdist + working-directory: ./robocorp/ + - run: pip wheel --no-deps . -w dist + working-directory: ./robocorp/ + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: robocorp/dist/ verbose: true diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index ad9b539..f7fb4db 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -6,7 +6,7 @@ on: - v* jobs: - build: + build-external-worker-client: runs-on: ubuntu-latest environment: release permissions: @@ -24,10 +24,40 @@ jobs: - run: source venv/bin/activate - run: pip install setuptools - run: python setup.py sdist + working-directory: ./external-worker/ - run: pip wheel --no-deps . -w dist + working-directory: ./external-worker/ - name: Publish package distributions to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ - packages-dir: dist/ + packages-dir: external-worker/dist/ + verbose: true + + build-robocorp-client: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12.0 + uses: actions/setup-python@v4 + with: + python-version: 3.12.0 + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - run: pip install virtualenv + - run: virtualenv venv + - run: source venv/bin/activate + - run: pip install setuptools + - run: python setup.py sdist + working-directory: ./robocorp/ + - run: pip wheel --no-deps . -w dist + working-directory: ./robocorp/ + - name: Publish package distributions to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: robocorp/dist/ verbose: true diff --git a/README.md b/README.md index 5ad614d..9b4f428 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,12 @@ -# Flowable External Worker Library for Python - +# Flowable External Client Python [License: ![license](https://img.shields.io/hexpm/l/plug.svg)](https://github.com/flowable/flowable-external-client-python/blob/main/LICENSE) ![Flowable Actions CI](https://github.com/flowable/flowable-external-client-python/actions/workflows/main.yml/badge.svg?branch=main) -An _External Worker Task_ in BPMN or CMMN is a task where the custom logic of that task is executed externally to Flowable, i.e. on another server. -When the process or case engine arrives at such a task, it will create an **external job**, which is exposed over the REST API. -Through this REST API, the job can be acquired and locked. -Once locked, the custom logic is responsible for signalling over REST that the work is done and the process or case can continue. - -This project makes implementing such custom logic in Python easy by not having the worry about the low-level details of the REST API and focus on the actual custom business logic. -Integrations for other languages are available, too. - -## Authentication - -The different ways to authenticate are explained in the [documentation of the underlying requests HTTP library which is used to connect to Flowable](https://requests.readthedocs.io/en/latest/user/authentication/). -The `ExternalWorkerClient` accepts as a parameter `auth` an implementation of `requests.auth.AuthBase`. -There are default implementations for example for basic authentication e.g. `HTTPBasicAuth("admin", "test")`. -Flowable offers a bearer token implementation `FlowableCloudToken` which allows to specify an access token to the Flowable Cloud offering. - -## Installation - -To install the external worker library, execute the following command: - -``` -pip install flowable.external-worker-client -``` - -## Sample - -### Cloud - -The usage with Flowable Cloud is simpler, since everything is pre-configured. -However, it's required to either use the user credentials or to pre-configure a personal access token. - -```python -from flowable.external_worker_client import ExternalWorkerClient -from flowable.external_worker_client.cloud_token import FlowableCloudToken - -client = ExternalWorkerClient(auth=FlowableCloudToken("")) - -def my_callback(job, worker_result_builder): - print('Executed job: ' + job.id) - return worker_result_builder.success() - -subscription = client.subscribe('myTopic', my_callback) -``` - -### Local - -The following is an example how you can connect to a Flowable instance running at `http://localhost:8090` and process all messages retrieved on the topic `myTopic`: - -```python -from flowable.external_worker_client import ExternalWorkerClient -from requests.auth import HTTPBasicAuth - -client = ExternalWorkerClient('http://localhost:8090/flowable-work', auth=HTTPBasicAuth("admin", "test")) - -def my_callback(job, worker_result_builder): - print('Executed job: ' + job.id) - return worker_result_builder.success() - -subscription = client.subscribe('myTopic', my_callback) -``` +This is a collection of multiple Flowable Python clients. +Currently, the project consists out of: +* [External Worker Client](./external-worker): A library to connect custom application code to Flowable with the external worker functionality +* [Robocorp Client](./robocorp-client): A python module to execute Robocorp actions and tasks with the Flowable Robocorp task (based on the external worker). \ No newline at end of file diff --git a/external-worker/README.md b/external-worker/README.md new file mode 100644 index 0000000..b1299c8 --- /dev/null +++ b/external-worker/README.md @@ -0,0 +1,67 @@ +# Flowable External Worker Library for Python + + +[License: +![license](https://img.shields.io/hexpm/l/plug.svg)](https://github.com/flowable/flowable-external-client-python/blob/main/LICENSE) + +![Flowable Actions CI](https://github.com/flowable/flowable-external-client-python/actions/workflows/main.yml/badge.svg?branch=main) + +An _External Worker Task_ in BPMN or CMMN is a task where the custom logic of that task is executed externally to Flowable, i.e. on another server. +When the process or case engine arrives at such a task, it will create an **external job**, which is exposed over the REST API. +Through this REST API, the job can be acquired and locked. +Once locked, the custom logic is responsible for signalling over REST that the work is done and the process or case can continue. + +This project makes implementing such custom logic in Python easy by not having the worry about the low-level details of the REST API and focus on the actual custom business logic. +Integrations for other languages are available, too. + +## Authentication + +The different ways to authenticate are explained in the [documentation of the underlying requests HTTP library which is used to connect to Flowable](https://requests.readthedocs.io/en/latest/user/authentication/). +The `ExternalWorkerClient` accepts as a parameter `auth` an implementation of `requests.auth.AuthBase`. +There are default implementations for example for basic authentication e.g. `HTTPBasicAuth("admin", "test")`. +Flowable offers a bearer token implementation `FlowableCloudToken` which allows to specify an access token to the Flowable Cloud offering. + +## Installation + +To install the external worker library, execute the following command: + +``` +pip install flowable.external-worker-client +``` + +## Sample + +### Cloud + +The usage with Flowable Cloud is simpler, since everything is pre-configured. +However, it's required to either use the user credentials or to pre-configure a personal access token. + +```python +from flowable.external_worker_client import ExternalWorkerClient +from flowable.external_worker_client.cloud_token import FlowableCloudToken + +client = ExternalWorkerClient(auth=FlowableCloudToken("")) + +def my_callback(job, worker_result_builder): + print('Executed job: ' + job.id) + return worker_result_builder.success() + +subscription = client.subscribe('myTopic', my_callback) +``` + +### Local + +The following is an example how you can connect to a Flowable instance running at `http://localhost:8090` and process all messages retrieved on the topic `myTopic`: + +```python +from flowable.external_worker_client import ExternalWorkerClient +from requests.auth import HTTPBasicAuth + +client = ExternalWorkerClient('http://localhost:8090/flowable-work', auth=HTTPBasicAuth("admin", "test")) + +def my_callback(job, worker_result_builder): + print('Executed job: ' + job.id) + return worker_result_builder.success() + +subscription = client.subscribe('myTopic', my_callback) +``` diff --git a/flowable/__init__.py b/external-worker/flowable/__init__.py similarity index 100% rename from flowable/__init__.py rename to external-worker/flowable/__init__.py diff --git a/flowable/external_worker_client/__init__.py b/external-worker/flowable/external_worker_client/__init__.py similarity index 91% rename from flowable/external_worker_client/__init__.py rename to external-worker/flowable/external_worker_client/__init__.py index f4ea696..33e775b 100644 --- a/flowable/external_worker_client/__init__.py +++ b/external-worker/flowable/external_worker_client/__init__.py @@ -1,3 +1,4 @@ +import sys import threading import time from os import getpid @@ -52,7 +53,7 @@ class ExternalWorkerClient(object): def __init__( self, - flowable_host: str = "https://cloud.flowable.com/work", + flowable_host: str = "https://trial.flowable.com/work", worker_id: str = None, auth: AuthBase = None, customize_session: Callable[[Session], None] = lambda session: None @@ -103,5 +104,7 @@ def _consume( result.execute(self._restClient) else: self._restClient.complete_job(job.id) - except: - self._restClient.fail_job(job.id) + except Exception as e: + print("An error occurred during job execution " + job.id, file=sys.stderr) + print(e, file=sys.stderr) + self._restClient.fail_job(job.id, e.__str__()) diff --git a/flowable/external_worker_client/cloud_token.py b/external-worker/flowable/external_worker_client/cloud_token.py similarity index 100% rename from flowable/external_worker_client/cloud_token.py rename to external-worker/flowable/external_worker_client/cloud_token.py diff --git a/flowable/external_worker_client/engine_rest_variable.py b/external-worker/flowable/external_worker_client/engine_rest_variable.py similarity index 100% rename from flowable/external_worker_client/engine_rest_variable.py rename to external-worker/flowable/external_worker_client/engine_rest_variable.py diff --git a/flowable/external_worker_client/external_worker_acquire_job_response.py b/external-worker/flowable/external_worker_client/external_worker_acquire_job_response.py similarity index 100% rename from flowable/external_worker_client/external_worker_acquire_job_response.py rename to external-worker/flowable/external_worker_client/external_worker_acquire_job_response.py diff --git a/flowable/external_worker_client/external_worker_job_response.py b/external-worker/flowable/external_worker_client/external_worker_job_response.py similarity index 100% rename from flowable/external_worker_client/external_worker_job_response.py rename to external-worker/flowable/external_worker_client/external_worker_job_response.py diff --git a/flowable/external_worker_client/flowable_rest_exception.py b/external-worker/flowable/external_worker_client/flowable_rest_exception.py similarity index 100% rename from flowable/external_worker_client/flowable_rest_exception.py rename to external-worker/flowable/external_worker_client/flowable_rest_exception.py diff --git a/flowable/external_worker_client/list_result.py b/external-worker/flowable/external_worker_client/list_result.py similarity index 100% rename from flowable/external_worker_client/list_result.py rename to external-worker/flowable/external_worker_client/list_result.py diff --git a/flowable/external_worker_client/request_converter.py b/external-worker/flowable/external_worker_client/request_converter.py similarity index 100% rename from flowable/external_worker_client/request_converter.py rename to external-worker/flowable/external_worker_client/request_converter.py diff --git a/flowable/external_worker_client/response_converter.py b/external-worker/flowable/external_worker_client/response_converter.py similarity index 100% rename from flowable/external_worker_client/response_converter.py rename to external-worker/flowable/external_worker_client/response_converter.py diff --git a/flowable/external_worker_client/restclient.py b/external-worker/flowable/external_worker_client/restclient.py similarity index 99% rename from flowable/external_worker_client/restclient.py rename to external-worker/flowable/external_worker_client/restclient.py index 8ea9480..100fbb0 100644 --- a/flowable/external_worker_client/restclient.py +++ b/external-worker/flowable/external_worker_client/restclient.py @@ -85,6 +85,7 @@ def acquire_jobs( } ) if r.status_code != 200: + print(r.text) raise FlowableRestException(r.status_code, r.text) return list(map(response_converter.convert_to_external_worker_acquire_job_response, r.json())) diff --git a/flowable/external_worker_client/utils.py b/external-worker/flowable/external_worker_client/utils.py similarity index 100% rename from flowable/external_worker_client/utils.py rename to external-worker/flowable/external_worker_client/utils.py diff --git a/flowable/external_worker_client/worker_result.py b/external-worker/flowable/external_worker_client/worker_result.py similarity index 100% rename from flowable/external_worker_client/worker_result.py rename to external-worker/flowable/external_worker_client/worker_result.py diff --git a/setup.py b/external-worker/setup.py similarity index 75% rename from setup.py rename to external-worker/setup.py index 98a61ca..d684bb7 100644 --- a/setup.py +++ b/external-worker/setup.py @@ -1,22 +1,22 @@ -from setuptools import find_packages, setup - # read the contents of your README file from pathlib import Path +from setuptools import setup + this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() setup( name='flowable.external-worker-client', packages=['flowable', 'flowable.external_worker_client'], - version='1.0.0', + version='1.0.1rc1', description='Flowable External Worker Library to connect Python code to Flowable using an external worker.', long_description=long_description, long_description_content_type='text/markdown', author='Flowable', - license='', + license='Apache License, Version 2.0', install_requires=['requests>=2.27.0'], - setup_requires=['pytest-runner'], - tests_require=['pytest', 'vcrpy'], - test_suite='tests', + extras_require={ + 'testing': ['pytest', 'vcrpy'] + }, ) diff --git a/tests/__init__.py b/external-worker/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to external-worker/tests/__init__.py diff --git a/tests/basic_test.py b/external-worker/tests/basic_test.py similarity index 100% rename from tests/basic_test.py rename to external-worker/tests/basic_test.py diff --git a/tests/bpmn_utils.py b/external-worker/tests/bpmn_utils.py similarity index 100% rename from tests/bpmn_utils.py rename to external-worker/tests/bpmn_utils.py diff --git a/tests/cmmn_utils.py b/external-worker/tests/cmmn_utils.py similarity index 100% rename from tests/cmmn_utils.py rename to external-worker/tests/cmmn_utils.py diff --git a/tests/fixtures/cases/externalWorkerCase.cmmn b/external-worker/tests/fixtures/cases/externalWorkerCase.cmmn similarity index 100% rename from tests/fixtures/cases/externalWorkerCase.cmmn rename to external-worker/tests/fixtures/cases/externalWorkerCase.cmmn diff --git a/tests/fixtures/cassettes/test_acquire_jobs.yml b/external-worker/tests/fixtures/cassettes/test_acquire_jobs.yml similarity index 100% rename from tests/fixtures/cassettes/test_acquire_jobs.yml rename to external-worker/tests/fixtures/cassettes/test_acquire_jobs.yml diff --git a/tests/fixtures/cassettes/test_complete_job.yml b/external-worker/tests/fixtures/cassettes/test_complete_job.yml similarity index 100% rename from tests/fixtures/cassettes/test_complete_job.yml rename to external-worker/tests/fixtures/cassettes/test_complete_job.yml diff --git a/tests/fixtures/cassettes/test_consume.yml b/external-worker/tests/fixtures/cassettes/test_consume.yml similarity index 100% rename from tests/fixtures/cassettes/test_consume.yml rename to external-worker/tests/fixtures/cassettes/test_consume.yml diff --git a/tests/fixtures/cassettes/test_consume_with_bpmn_error.yml b/external-worker/tests/fixtures/cassettes/test_consume_with_bpmn_error.yml similarity index 100% rename from tests/fixtures/cassettes/test_consume_with_bpmn_error.yml rename to external-worker/tests/fixtures/cassettes/test_consume_with_bpmn_error.yml diff --git a/tests/fixtures/cassettes/test_consume_with_cmmn_terminate.yml b/external-worker/tests/fixtures/cassettes/test_consume_with_cmmn_terminate.yml similarity index 100% rename from tests/fixtures/cassettes/test_consume_with_cmmn_terminate.yml rename to external-worker/tests/fixtures/cassettes/test_consume_with_cmmn_terminate.yml diff --git a/tests/fixtures/cassettes/test_fail_job.yml b/external-worker/tests/fixtures/cassettes/test_fail_job.yml similarity index 100% rename from tests/fixtures/cassettes/test_fail_job.yml rename to external-worker/tests/fixtures/cassettes/test_fail_job.yml diff --git a/tests/fixtures/cassettes/test_get_job.yml b/external-worker/tests/fixtures/cassettes/test_get_job.yml similarity index 100% rename from tests/fixtures/cassettes/test_get_job.yml rename to external-worker/tests/fixtures/cassettes/test_get_job.yml diff --git a/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_1_execute_first.yml b/external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_1_execute_first.yml similarity index 100% rename from tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_1_execute_first.yml rename to external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_1_execute_first.yml diff --git a/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_2_execute_second.yml b/external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_2_execute_second.yml similarity index 100% rename from tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_2_execute_second.yml rename to external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_2_execute_second.yml diff --git a/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_execute_first.yml b/external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_execute_first.yml similarity index 100% rename from tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_execute_first.yml rename to external-worker/tests/fixtures/cassettes/test_job_with_bpmn_error_without_error_code_execute_first.yml diff --git a/tests/fixtures/cassettes/test_job_with_cmmn_terminate.yml b/external-worker/tests/fixtures/cassettes/test_job_with_cmmn_terminate.yml similarity index 100% rename from tests/fixtures/cassettes/test_job_with_cmmn_terminate.yml rename to external-worker/tests/fixtures/cassettes/test_job_with_cmmn_terminate.yml diff --git a/tests/fixtures/cassettes/test_list_jobs.yml b/external-worker/tests/fixtures/cassettes/test_list_jobs.yml similarity index 100% rename from tests/fixtures/cassettes/test_list_jobs.yml rename to external-worker/tests/fixtures/cassettes/test_list_jobs.yml diff --git a/tests/fixtures/cassettes/test_list_jobs_customize_session.yml b/external-worker/tests/fixtures/cassettes/test_list_jobs_customize_session.yml similarity index 100% rename from tests/fixtures/cassettes/test_list_jobs_customize_session.yml rename to external-worker/tests/fixtures/cassettes/test_list_jobs_customize_session.yml diff --git a/tests/fixtures/cassettes/test_list_jobs_invalid_auth.yml b/external-worker/tests/fixtures/cassettes/test_list_jobs_invalid_auth.yml similarity index 100% rename from tests/fixtures/cassettes/test_list_jobs_invalid_auth.yml rename to external-worker/tests/fixtures/cassettes/test_list_jobs_invalid_auth.yml diff --git a/tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml b/external-worker/tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml similarity index 95% rename from tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml rename to external-worker/tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml index 4ff5326..0a30bcb 100644 --- a/tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml +++ b/external-worker/tests/fixtures/cassettes/test_subscribe_with_bpmn_error.yml @@ -118,8 +118,6 @@ interactions: - no-cache Set-Cookie: - XSRF-TOKEN=7315a015-53c4-4c28-ba52-4b070af2e92d; Path=/ - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -169,8 +167,6 @@ interactions: - no-cache Set-Cookie: - XSRF-TOKEN=43804e08-2ad4-4474-9aab-f1705f53da99; Path=/ - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -224,8 +220,6 @@ interactions: - no-cache Set-Cookie: - XSRF-TOKEN=0c1488a0-a248-4d13-9c37-d802fb441c1f; Path=/ - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -280,8 +274,6 @@ interactions: - timeout=60 Pragma: - no-cache - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -383,8 +375,6 @@ interactions: - timeout=60 Pragma: - no-cache - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -481,8 +471,6 @@ interactions: - no-cache Set-Cookie: - XSRF-TOKEN=1f64bb78-a592-4dfc-af73-38f79444b198; Path=/ - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -533,8 +521,6 @@ interactions: - no-cache Set-Cookie: - XSRF-TOKEN=9a7c42f5-b841-4b82-8b3c-b867e57abe63; Path=/ - Transfer-Encoding: - - chunked X-Content-Type-Options: - nosniff X-Frame-Options: @@ -592,4 +578,59 @@ interactions: status: code: 204 message: '' +- request: + body: '{"topic": "myTopic", "lockDuration": "PT1M", "numberOfTasks": 1, "numberOfRetries": + 5, "workerId": "test-worker", "scopeType": null}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + Content-Length: + - '132' + Content-Type: + - application/json + Cookie: + - XSRF-TOKEN=94293f77-1ef7-4280-8e87-c6e3aec5682f + User-Agent: + - python-requests/2.32.3 + method: POST + uri: http://localhost:8090/external-job-api/acquire/jobs + response: + body: + string: '[]' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sat, 07 Sep 2024 09:03:21 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '2' + status: + code: 200 + message: '' version: 1 diff --git a/tests/fixtures/processes/externalWorkerProcess.bpmn b/external-worker/tests/fixtures/processes/externalWorkerProcess.bpmn similarity index 100% rename from tests/fixtures/processes/externalWorkerProcess.bpmn rename to external-worker/tests/fixtures/processes/externalWorkerProcess.bpmn diff --git a/tests/fixtures/processes/withBoundaryEvent.bpmn b/external-worker/tests/fixtures/processes/withBoundaryEvent.bpmn similarity index 100% rename from tests/fixtures/processes/withBoundaryEvent.bpmn rename to external-worker/tests/fixtures/processes/withBoundaryEvent.bpmn diff --git a/tests/test_init.py b/external-worker/tests/test_init.py similarity index 100% rename from tests/test_init.py rename to external-worker/tests/test_init.py diff --git a/tests/test_restclient.py b/external-worker/tests/test_restclient.py similarity index 100% rename from tests/test_restclient.py rename to external-worker/tests/test_restclient.py diff --git a/tests/vcr.py b/external-worker/tests/vcr.py similarity index 100% rename from tests/vcr.py rename to external-worker/tests/vcr.py diff --git a/robocorp/README.md b/robocorp/README.md new file mode 100644 index 0000000..7cb219b --- /dev/null +++ b/robocorp/README.md @@ -0,0 +1,74 @@ +from typing import Dict# Flowable Robocorp Client + +[License: +![license](https://img.shields.io/hexpm/l/plug.svg)](https://github.com/flowable/flowable-external-client-python/blob/main/LICENSE) + +![Flowable Actions CI](https://github.com/flowable/flowable-external-client-python/actions/workflows/main.yml/badge.svg?branch=main) + +This is a python module to connect a Flowable installation through an external worker with a Robocorp task or action. +This allows to execute a Robocorp task/action from a business process with BPMN or a case diagram CMMN. +The application is started with a task/action definition from Robocorp and connects to an external worker topic. +When using the Robocorp task in Flowable, it is possible to specify a task/action name. +In the task, the specific task/action specified will be executed and the result (only for actions) will be sent back to the process. +It is required that the variables in the diagram match exactly the variables inside the robocorp task/action. + +## Installation + +To install the Flowable Robocorp client, execute the following command: + +``` +pip install flowable.robocorp-client +``` + +## Sample + +The following [diagram BPMN](docs/robocorpExample.bpmn) illustrates a basic usage of the Robocorp task: + +![Simple BPMN diagram with a start event, a robocorp task and an end event](docs/robocorpExample.png) + +The following example `get-weather-forecast.py` can be used as a Robocorp action: +```python +from robocorp.actions import action + +@action +def get_weather_forecast(city: str, days: int, scale: str = "celsius") -> dict[str, str]: + """ + Returns weather conditions forecast for a given city. + + Args: + city (str): Target city to get the weather conditions for + days: How many day forecast to return + scale (str): Temperature scale to use, should be one of "celsius" or "fahrenheit" + + Returns: + object: The requested weather conditions forecast with the temperature as one element + """ + + # your implementation goes here... + + return { + "temperature": "the weather will be nice" + } +``` + +The Robocorp worker can be started with the following command: +```sh +python -m flowable.robocorp_client --flowable-token myTopic action get-weather-forecast.py +``` + +You can request `` at the [Flowable Trial](https://trial.flowable.com/work/) by clicking at the bottom left on your user and go to Settings. +Once in the settings, choose "Access Token" and press the button "New token". +After providing name and the validity, the token is generated and can be set. + +Once the module is started and a process instance is created, it will automatically execute the Robocorp action and returns the result to Flowable. +The Python module will keep running until stopped to process potential additional jobs. + +## Authentication + +Next to the cloud authentication, it's possible to authenticate against other hosts. +The hostname can be specified with `--flowable-host`. + +For the authentication there are two possibilities: + +1. Providing a bearer token with the `--flowable-token ` attribute. +2. Providing basic authentication with `--flowable-username ` and `--flowable-password ` diff --git a/robocorp/docs/robocorpExample.bpmn b/robocorp/docs/robocorpExample.bpmn new file mode 100644 index 0000000..b9d47e1 --- /dev/null +++ b/robocorp/docs/robocorpExample.bpmn @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/robocorp/docs/robocorpExample.png b/robocorp/docs/robocorpExample.png new file mode 100644 index 0000000000000000000000000000000000000000..c329be3f5d5e214a655b533b9877aa84c2458be5 GIT binary patch literal 13437 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 Start consuming for topic "' + topic + '"') + client = ExternalWorkerClient(args.flowable_host, auth=auth) + + if args.mode == 'action': + robocorp_job_handler = RobocorpActionHandler(robocorp_action_file) + else: + robocorp_job_handler = RobocorpTaskHandler(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 new file mode 100644 index 0000000..39a5000 --- /dev/null +++ b/robocorp/flowable/robocorp_client/call_robocorp.py @@ -0,0 +1,12 @@ +import subprocess +import sys + + +def call_robocorp(args, mod_name='robocorp.actions'): + call_args = [sys.executable, "-m", mod_name] + call_args.extend(args) + try: + return subprocess.run(call_args, capture_output=True, text=True) + 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 new file mode 100644 index 0000000..1102b16 --- /dev/null +++ b/robocorp/flowable/robocorp_client/robocorp_handler.py @@ -0,0 +1,102 @@ +import ast +import json +import shlex + +from flowable.external_worker_client import ExternalWorkerAcquireJobResponse, WorkerResultBuilder +from flowable.robocorp_client.call_robocorp import call_robocorp + + +def extract_action_and_parameters(job): + action = None + params = [] + for variable in job.variables: + if variable.name == '__robocorpTaskName': + action = variable.value + else: + if variable.value is not None: + params.append('--' + shlex.quote(variable.name) + '=' + shlex.quote(variable.value.__str__())) + return action, params + + +def extract_result(results): + result_item = None + lines = results.strip().splitlines() + for line in lines: + parsed = json.loads(line) + if parsed['message_type'] == 'R': + result_item = parsed + result = None + if result_item is not None: + result = result_item['value'] + return result + + +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) + + if isinstance(json_result, dict): + for key in json_result: + value = json_result[key] + if isinstance(value, str): + work_result.variable_string(key, value) + elif isinstance(value, int): + work_result.variable_int(key, value) + elif isinstance(value, float): + work_result.variable_float(key, value) + elif isinstance(value, bool): + work_result.variable_boolean(key, value) + elif isinstance(value, object): + work_result.variable_json(key, value) + else: + work_result.variable_string(key, str(value)) + else: + work_result.variable_json('result', json_result) + return work_result + + +class RobocorpActionHandler: + 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(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 = ['run', self.robocorp_action_file, '-a' + action.__str__(), '--log-output-to-stdout=json', '-o' + output_dir, '--'] + robocorp_args.extend(params) + print('---> Execute job "' + job.id + '"', robocorp_args) + results = call_robocorp(robocorp_args) + if results.returncode != 0: + print('---> Job execution failed for "' + job.id + '". Output saved to ' + output_dir) + return worker_result_builder.failure().error_message('failed with status code ' + str(results.returncode)).error_details(results.stderr + results.stdout) + 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) + + +class RobocorpTaskHandler: + 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(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 = ['run', self.robocorp_action_file, '-t' + action.__str__(), '-o' + output_dir, '--'] + robocorp_args.extend(params) + print('---> Execute job "' + job.id + '"', robocorp_args) + results = call_robocorp(robocorp_args, mod_name='robocorp.tasks') + if results.returncode != 0: + print('---> Job execution failed for "' + job.id + '". Output saved to ' + output_dir) + 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() diff --git a/robocorp/setup.py b/robocorp/setup.py new file mode 100644 index 0000000..9f21e17 --- /dev/null +++ b/robocorp/setup.py @@ -0,0 +1,22 @@ +# read the contents of your README file +from pathlib import Path + +from setuptools import setup + +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +setup( + name='flowable.robocorp-client', + packages=['flowable', 'flowable.robocorp_client'], + version='1.0.1rc1', + description='Flowable client to be used with Robocorp. This client connects to a Flowable instance via external worker and executes robocorp tasks or actions.', + long_description=long_description, + long_description_content_type='text/markdown', + author='Flowable', + license='Apache License, Version 2.0', + install_requires=['flowable.external-worker-client>=1.0.1rc1', 'robocorp-actions>=0.2.1', 'robocorp-tasks>=3.1.1', 'robocorp-truststore>=0.9.1'], + extras_require={ + 'testing': ['pytest', 'vcrpy', 'flowable.external-worker-client>=1.0.1rc1'] + }, +) diff --git a/robocorp/tests/__init__.py b/robocorp/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/robocorp/tests/basic_test.py b/robocorp/tests/basic_test.py new file mode 100644 index 0000000..1b414f4 --- /dev/null +++ b/robocorp/tests/basic_test.py @@ -0,0 +1,20 @@ +import unittest +from pathlib import Path + +from requests.auth import HTTPBasicAuth + +from tests.bpmn_utils import deploy_process, get_process_definition_id, delete_deployment + +base_url = "http://localhost:8090/flowable-work" +auth = HTTPBasicAuth("admin", "test") + + +class BasicTest(unittest.TestCase): + + def deploy_process(self, fileToDeploy='externalWorkerProcess.bpmn'): + self._process_deployment_id = deploy_process(base_url, auth, str(Path(__file__).parent.absolute()) + '/fixtures/processes/' + fileToDeploy) + self._process_definition_id = get_process_definition_id(base_url, auth, self._process_deployment_id) + + def remove_deployment(self): + if hasattr(self, "_process_deployment_id"): + delete_deployment(base_url, auth, self._process_deployment_id) diff --git a/robocorp/tests/bpmn_utils.py b/robocorp/tests/bpmn_utils.py new file mode 100644 index 0000000..9e855d3 --- /dev/null +++ b/robocorp/tests/bpmn_utils.py @@ -0,0 +1,55 @@ +import requests +from requests.auth import AuthBase + + +def deploy_process(base_url: str, auth: AuthBase, file: str) -> str: + files = {'file': open(file, 'rb')} + deployment_result = requests.post(base_url + '/process-api/repository/deployments', auth=auth, files=files) + assert deployment_result.status_code == 201 + return deployment_result.json().get("id") + + +def delete_deployment(base_url: str, auth: AuthBase, process_deployment_id: str) -> None: + delete_deployment_result = requests.delete(base_url + "/process-api/repository/deployments/" + process_deployment_id, auth=auth) + assert delete_deployment_result.status_code == 204 + + +def get_process_definition_id(base_url: str, auth: AuthBase, process_deployment_id: str) -> None: + definition_search = requests.get(base_url + "/process-api/repository/process-definitions?deploymentId=" + process_deployment_id, auth=auth) + assert definition_search.status_code == 200 + data = definition_search.json().get("data") + assert len(data) == 1 + return data[0].get("id") + + +def start_process(base_url: str, auth: AuthBase, process_definition_id: str, variables: list[object] = []) -> str: + start_result = requests.post( + base_url + "/process-api/runtime/process-instances", + auth=auth, + json={"processDefinitionId": process_definition_id, "variables": variables}, + headers={"Content-Type": "application/json"} + ) + assert start_result.status_code == 201 + return start_result.json().get("id") + + +def terminate_process(base_url: str, auth: AuthBase, process_instance_id: str) -> None: + delete_result = requests.delete(base_url + "/process-api/runtime/process-instances/" + process_instance_id, auth=auth) + assert delete_result.status_code == 204 or delete_result.status_code == 404 + + +def get_process_variable(base_url: str, auth: AuthBase, process_instance_id: str, variable_name: str) -> dict | None: + variable_result = requests.get(base_url + "/process-api/history/historic-variable-instances?processInstanceId=" + process_instance_id + "&variableName=" + variable_name, auth=auth) + assert variable_result.status_code == 200 + data = variable_result.json().get("data") + if len(data) == 1: + return data[0].get("variable") + else: + return None + + +def executed_activity_ids(base_url: str, auth: AuthBase, process_instance_id: str) -> list[str]: + activity_instances = requests.get(base_url + "/process-api/history/historic-activity-instances?processInstanceId=" + process_instance_id, auth=auth) + assert activity_instances.status_code == 200 + data = activity_instances.json().get("data") + return sorted(list(map(lambda o: o.get("activityId"), data))) diff --git a/robocorp/tests/fixtures/cassettes/test_with_custom_action.yml b/robocorp/tests/fixtures/cassettes/test_with_custom_action.yml new file mode 100644 index 0000000..80ed24a --- /dev/null +++ b/robocorp/tests/fixtures/cassettes/test_with_custom_action.yml @@ -0,0 +1,618 @@ +interactions: +- request: + body: "--7d6d74e28e95ea697b80fb2e866f3861\r\nContent-Disposition: form-data; name=\"file\"; + filename=\"robocorpExample.bpmn\"\r\n\r\n\n\n + \ \n \n + \ \n \n + \ \n + \ \n \n + \ \n \n \n \n + \ \n + \ \n \n + \ \n \n \n \n + \ \n + \ \n \n + \ \n \n \n + \ \n \n + \ \n \n \n \n + \ \n \n \n \n + \ \n \n + \ \n \n \n + \ \n + \ \n + \ \n + \ \n \n \n \n \n \n \n + \ \n \n + \ \n \n + \ \n \n + \ \n \n + \ \n \n\r\n--7d6d74e28e95ea697b80fb2e866f3861--\r\n" + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + Content-Length: + - '4594' + Content-Type: + - multipart/form-data; boundary=7d6d74e28e95ea697b80fb2e866f3861 + User-Agent: + - python-requests/2.32.3 + method: POST + uri: http://localhost:8090/flowable-work/process-api/repository/deployments + response: + body: + string: '{"id":"PRC-d7476565-7789-11ef-8757-fe6a9bab3681","name":"robocorpExample","deploymentTime":"2024-09-20T21:52:17.778+02:00","category":null,"parentDeploymentId":"PRC-d7476565-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/process-api/repository/deployments/PRC-d7476565-7789-11ef-8757-fe6a9bab3681","tenantId":""}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=62423fb1-697e-413c-9b3d-3f24f0e9780b; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '337' + status: + code: 201 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/process-api/repository/process-definitions?deploymentId=PRC-d7476565-7789-11ef-8757-fe6a9bab3681 + response: + body: + string: '{"data":[{"id":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/process-api/repository/process-definitions/PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","key":"robocorpExample","version":3,"name":"Robocorp + Example","description":null,"tenantId":"","deploymentId":"PRC-d7476565-7789-11ef-8757-fe6a9bab3681","deploymentUrl":"http://localhost:8090/flowable-work/process-api/repository/deployments/PRC-d7476565-7789-11ef-8757-fe6a9bab3681","resource":"http://localhost:8090/flowable-work/process-api/repository/deployments/PRC-d7476565-7789-11ef-8757-fe6a9bab3681/resources/robocorpExample.bpmn","diagramResource":null,"category":"http://flowable.org/test","graphicalNotationDefined":true,"suspended":false,"startFormDefined":false}],"total":1,"start":0,"sort":"name","order":"asc","size":1}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=73278ac4-c116-42b6-bee9-73ed4296f913; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '856' + status: + code: 200 + message: '' +- request: + body: '{"processDefinitionId": "PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681", + "variables": [{"name": "city", "type": "string", "value": "Zurich"}, {"name": + "days", "type": "integer", "value": 3}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + Content-Length: + - '204' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.3 + method: POST + uri: http://localhost:8090/flowable-work/process-api/runtime/process-instances + response: + body: + string: '{"id":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/process-api/runtime/process-instances/PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","name":null,"businessKey":null,"businessStatus":null,"suspended":false,"ended":false,"processDefinitionId":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","processDefinitionUrl":"http://localhost:8090/flowable-work/process-api/repository/process-definitions/PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","processDefinitionName":"Robocorp + Example","processDefinitionDescription":null,"activityId":null,"startUserId":"admin","startTime":"2024-09-20T21:52:18.044+02:00","superProcessInstanceId":null,"variables":[{"name":"city","type":"string","value":"Zurich","scope":"local"},{"name":"initiator","type":"string","value":"admin","scope":"local"},{"name":"days","type":"integer","value":3,"scope":"local"}],"callbackId":null,"callbackType":null,"referenceId":null,"referenceType":null,"propagatedStageInstanceId":null,"tenantId":"","completed":false}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=051033f2-6205-488e-84e9-aa4140c00722; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '1046' + status: + code: 201 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/external-job-api/jobs?processInstanceId=PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681 + response: + body: + string: '{"data":[{"id":"JOB-d7707145-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681","correlationId":"d7707144-7789-11ef-8757-fe6a9bab3681","processInstanceId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","processDefinitionId":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","executionId":"PRC-d770231e-7789-11ef-8757-fe6a9bab3681","scopeId":null,"subScopeId":null,"scopeDefinitionId":null,"scopeType":null,"elementId":"bpmnTask_1","elementName":"Get + Weather Forecast","retries":3,"exceptionMessage":null,"dueDate":null,"createTime":"2024-09-20T19:52:18.047Z","tenantId":"","lockOwner":null,"lockExpirationTime":null}],"total":1,"start":0,"sort":"id","order":"asc","size":1}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=8d8dec72-f0fc-4452-a82c-a18f107e41c9; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '765' + status: + code: 200 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681?i=0 + response: + body: + string: '{"id":"JOB-d7707145-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681","correlationId":"d7707144-7789-11ef-8757-fe6a9bab3681","processInstanceId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","processDefinitionId":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","executionId":"PRC-d770231e-7789-11ef-8757-fe6a9bab3681","scopeId":null,"subScopeId":null,"scopeDefinitionId":null,"scopeType":null,"elementId":"bpmnTask_1","elementName":"Get + Weather Forecast","retries":3,"exceptionMessage":null,"dueDate":null,"createTime":"2024-09-20T19:52:18.047Z","tenantId":"","lockOwner":null,"lockExpirationTime":null}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=29769a42-9c6e-476b-8e5c-924e7cdf37fd; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '699' + status: + code: 200 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681?i=1 + response: + body: + string: '{"id":"JOB-d7707145-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681","correlationId":"d7707144-7789-11ef-8757-fe6a9bab3681","processInstanceId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","processDefinitionId":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","executionId":"PRC-d770231e-7789-11ef-8757-fe6a9bab3681","scopeId":null,"subScopeId":null,"scopeDefinitionId":null,"scopeType":null,"elementId":"bpmnTask_1","elementName":"Get + Weather Forecast","retries":3,"exceptionMessage":null,"dueDate":null,"createTime":"2024-09-20T19:52:18.047Z","tenantId":"","lockOwner":"lelia.local-44317","lockExpirationTime":"2024-09-20T19:53:18.540Z"}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:17 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=d9451b5a-7346-4344-8983-65b324b94404; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '736' + status: + code: 200 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681?i=2 + response: + body: + string: '{"id":"JOB-d7707145-7789-11ef-8757-fe6a9bab3681","url":"http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681","correlationId":"d7707144-7789-11ef-8757-fe6a9bab3681","processInstanceId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","processDefinitionId":"PRC-robocorpExample:3:d749d667-7789-11ef-8757-fe6a9bab3681","executionId":"PRC-d770231e-7789-11ef-8757-fe6a9bab3681","scopeId":null,"subScopeId":null,"scopeDefinitionId":null,"scopeType":null,"elementId":"bpmnTask_1","elementName":"Get + Weather Forecast","retries":3,"exceptionMessage":null,"dueDate":null,"createTime":"2024-09-20T19:52:18.047Z","tenantId":"","lockOwner":"lelia.local-44317","lockExpirationTime":"2024-09-20T19:53:18.540Z"}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:18 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=3045f0c1-f8f9-4375-9400-d8cf7d519b57; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '736' + status: + code: 200 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/external-job-api/jobs/JOB-d7707145-7789-11ef-8757-fe6a9bab3681?i=3 + response: + body: + string: '{"message":"Not found","exception":"Could not find external worker + job with id ''JOB-d7707145-7789-11ef-8757-fe6a9bab3681''."}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:18 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=ebf40149-a726-467b-b666-a2ee794e55b5; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '124' + status: + code: 404 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + User-Agent: + - python-requests/2.32.3 + method: GET + uri: http://localhost:8090/flowable-work/process-api/history/historic-variable-instances?processInstanceId=PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681&variableName=temperature + response: + body: + string: '{"data":[{"id":"VAR-d81f9d09-7789-11ef-8757-fe6a9bab3681","processInstanceId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","processInstanceUrl":"http://localhost:8090/flowable-work/process-api/history/historic-process-instances/PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","taskId":null,"executionId":"PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681","variable":{"name":"temperature","type":"integer","value":18,"scope":"global"}}],"total":1,"start":0,"sort":"variableName","order":"asc","size":1}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:18 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=118c41ad-05fe-4c95-8282-b11ef9433cfc; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '485' + status: + code: 200 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.32.3 + method: DELETE + uri: http://localhost:8090/flowable-work/process-api/runtime/process-instances/PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681 + response: + body: + string: '{"message":"Not found","exception":"Could not find a process instance + with id ''PRC-d76ffc09-7789-11ef-8757-fe6a9bab3681''."}' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Fri, 20 Sep 2024 19:52:18 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=544145b7-2c6a-4f55-861d-1aecc58aea32; Path=/flowable-work + Transfer-Encoding: + - chunked + Vary: + - origin,access-control-request-method,access-control-request-headers,accept-encoding + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + content-length: + - '123' + status: + code: 404 + message: '' +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - Basic YWRtaW46dGVzdA== + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.32.3 + method: DELETE + uri: http://localhost:8090/flowable-work/process-api/repository/deployments/PRC-d7476565-7789-11ef-8757-fe6a9bab3681 + response: + body: + string: '' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Date: + - Fri, 20 Sep 2024 19:52:18 GMT + Expires: + - '0' + Keep-Alive: + - timeout=60 + Pragma: + - no-cache + Set-Cookie: + - XSRF-TOKEN=f2d2e2e7-ad28-407c-9759-9d471b06d820; Path=/flowable-work + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 204 + message: '' +version: 1 diff --git a/robocorp/tests/fixtures/processes/robocorpExample.bpmn b/robocorp/tests/fixtures/processes/robocorpExample.bpmn new file mode 100644 index 0000000..b9d47e1 --- /dev/null +++ b/robocorp/tests/fixtures/processes/robocorpExample.bpmn @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/robocorp/tests/robocorp_action_weather.py b/robocorp/tests/robocorp_action_weather.py new file mode 100644 index 0000000..3aa2835 --- /dev/null +++ b/robocorp/tests/robocorp_action_weather.py @@ -0,0 +1,8 @@ +from robocorp.actions import action + +@action +def get_weather_forecast(city: str, days: int, scale: str = "celsius") -> dict[str, int]: + # Just a random implementation which multiplies the length of the city by the days + return { + "temperature": days * len(city) + } \ No newline at end of file diff --git a/robocorp/tests/test_robocorp_client.py b/robocorp/tests/test_robocorp_client.py new file mode 100644 index 0000000..0a95ca1 --- /dev/null +++ b/robocorp/tests/test_robocorp_client.py @@ -0,0 +1,57 @@ +import os +import subprocess +import sys +from time import sleep + +import requests + +from tests.basic_test import BasicTest, base_url, auth +from tests.bpmn_utils import start_process, terminate_process, get_process_variable + +from tests.vcr import my_vcr + + +class TestRestClient(BasicTest): + @my_vcr.use_cassette + def test_with_custom_action(self): + self.deploy_process('robocorpExample.bpmn') + process_instance_id = start_process(base_url, auth, self._process_definition_id, [ + {'name': 'city', 'type': 'string', 'value': 'Zurich'}, + {'name': 'days', 'type': 'integer', 'value': 3} + ]) + try: + call_args = [sys.executable, "-m", 'flowable.robocorp_client', + '--flowable-host', base_url, + '--flowable-username', auth.username, + '--flowable-password', auth.password, + 'myTopic', + 'action', + os.path.join(os.path.dirname(os.path.abspath(__file__)), 'robocorp_action_weather.py') + ] + print(" ".join(call_args)) + + r = requests.get(base_url + '/external-job-api/jobs?processInstanceId=' + process_instance_id, auth=auth) + result = r.json() + job = result.get('data')[0].get('id') + + print('start subprocess with parameters', call_args) + process = subprocess.Popen(call_args, text=True) + i = 0 + while i < 10 and job is not None: + # we are adding the i that vcr is not assuming it's the same request + r = requests.get(base_url + '/external-job-api/jobs/' + job + '?i=' + str(i), auth=auth) + if r.status_code == 200: + print('waiting for job to complete', job) + sleep(0.2) + else: + job = None + i += 1 + + process.terminate() + + temperature = get_process_variable(base_url, auth, process_instance_id, 'temperature') + self.assertIsNotNone(temperature) + self.assertEqual(temperature['value'], 18) + finally: + terminate_process(base_url, auth, process_instance_id) + self.remove_deployment() diff --git a/robocorp/tests/vcr.py b/robocorp/tests/vcr.py new file mode 100644 index 0000000..dbf8235 --- /dev/null +++ b/robocorp/tests/vcr.py @@ -0,0 +1,12 @@ +from pathlib import Path + +import vcr +from vcr import VCR + +my_vcr = vcr.VCR( + cassette_library_dir=str(Path(__file__).parent.absolute()) + '/fixtures/cassettes', + path_transformer=VCR.ensure_suffix('.yml'), + # see https://vcrpy.readthedocs.io/en/latest/usage.html for further details; e.g. 'once' (for just at the beginning) or 'all' (for always) + record_mode='new_episodes', + decode_compressed_response=True, +)