├── tests ├── resources │ ├── test.txt │ └── rf-logo.png ├── bpmn │ ├── retouren_erfassen.png │ ├── evaluate_decision.dmn │ ├── message_test.bpmn │ └── demo_for_robot.bpmn ├── robot │ ├── config.py │ ├── config_cicd.py │ ├── Version │ │ └── test_get_version.robot │ ├── cleanup.resource │ ├── manual_fetch_and_lock.robot │ ├── ExternalTask │ │ ├── test_unlock.robot │ │ ├── test_get_recent_process_instance.robot │ │ ├── test_bpmn_error.robot │ │ ├── test_get_amount_workloads.robot │ │ ├── test_fetch_and_lock.robot │ │ ├── test_dict_vars_to_json.robot │ │ └── test_notify_failure.robot │ ├── test_camunda_url.robot │ ├── ProcessInstance │ │ ├── test_get_all_instances.robot │ │ ├── test_delete_processes.robot │ │ ├── test_get_activity_instance.robot │ │ └── test_get_process_instance_variable.robot │ ├── ProcessDefinition │ │ └── test_get_process_definitions.robot │ ├── Deployment │ │ └── test_deploy_bpmn.robot │ ├── Decision │ │ └── test_evaluate_decision.robot │ ├── Message │ │ └── deliver_message.robot │ └── Execution │ │ └── test_start_process.robot └── form │ └── embeddedSampleForm.html ├── CamundaLibrary ├── __init__.py ├── CamundaResources.py └── CamundaLibrary.py ├── requirements.txt ├── Dockerfile ├── .gitignore ├── .vscode └── launch.json ├── .github └── workflows │ ├── python-package.yml │ ├── robot-test.yml │ └── codeql-analysis.yml ├── libdoc └── generate_libdoc.py ├── setup.py ├── docs └── Introduction.md ├── .gitlab-ci.yml ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE /tests/resources/test.txt: -------------------------------------------------------------------------------- 1 | This is a test file for a camunda process. -------------------------------------------------------------------------------- /CamundaLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | from .CamundaLibrary import CamundaLibrary 2 | from .CamundaResources import CamundaResources -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework>=3.2 2 | generic-camunda-client>=7.15.0 3 | requests 4 | requests_toolbelt 5 | url-normalize 6 | -------------------------------------------------------------------------------- /tests/resources/rf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-camunda/HEAD/tests/resources/rf-logo.png -------------------------------------------------------------------------------- /tests/bpmn/retouren_erfassen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarketSquare/robotframework-camunda/HEAD/tests/bpmn/retouren_erfassen.png -------------------------------------------------------------------------------- /tests/robot/config.py: -------------------------------------------------------------------------------- 1 | configuration = { 2 | 'host' : 'http://localhost:8080', 3 | 'username': 'demo', 4 | 'password': 'demo' 5 | } -------------------------------------------------------------------------------- /tests/robot/config_cicd.py: -------------------------------------------------------------------------------- 1 | configuration = { 2 | 'host' : 'http://camunda:8080', 3 | 'username': 'demo', 4 | 'password': 'demo' 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-buster as runtime 2 | 3 | COPY requirements.txt /app/requirements.txt 4 | WORKDIR /app 5 | RUN pip install --upgrade --requirement requirements.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | output/ 3 | logs/ 4 | log/ 5 | temp/ 6 | tmp/ 7 | build/ 8 | .idea/ 9 | public/ 10 | lsp/ 11 | __pycache__/ 12 | .vscode/ 13 | 14 | *.egg-info 15 | *.log 16 | *.zip 17 | .robocop 18 | log.html 19 | report.html 20 | output.xml 21 | *.pyc -------------------------------------------------------------------------------- /tests/form/embeddedSampleForm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 8 |
9 |
10 | 11 | 14 |
15 |
-------------------------------------------------------------------------------- /tests/robot/Version/test_get_version.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library String 4 | Suite Setup Set Camunda Configuration ${configuration} 5 | 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | 10 | 11 | *** Test Cases *** 12 | There Should Be A Version String 13 | ${version_dto} Get Version 14 | ${version} Set Variable ${version_dto.version} 15 | ${matches} Get Regexp Matches ${version} \\d+\\.\\d+\\.\\d+ 16 | Length Should Be ${matches} 1 17 | -------------------------------------------------------------------------------- /tests/robot/cleanup.resource: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary ${CAMUNDA_HOST} 3 | 4 | *** Variables *** 5 | ${CAMUNDA_HOST} http://localhost:8080 6 | 7 | *** Keywords *** 8 | Delete all instances from process '${process_key}' 9 | ${instances} Get all active process instances ${process_key} 10 | FOR ${instance} IN @{instances} 11 | Delete process instance ${instance}[id] 12 | END 13 | ${instances} Get all active process instances ${process_key} 14 | Should Be Empty ${instances} Process ${process_key} should not have any processes anymore. 15 | -------------------------------------------------------------------------------- /tests/robot/manual_fetch_and_lock.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | 5 | *** Test Cases *** 6 | Test 'fetch workload' for non existing topic 7 | [Setup] set camunda url http://localhost:8080 8 | # GIVEN 9 | ${existing_topic} Set Variable process_demo_element 10 | 11 | # WHEN 12 | ${work_items} fetch workload topic=${existing_topic} 13 | 14 | # THEN 15 | #Should Not Be Empty ${work_items} 16 | 17 | ${recent_task} get fetch response 18 | log Recent task:\t${recent_task} 19 | 20 | ${my_result} Create Dictionary lastname=Stahl 21 | complete task ${my_result} 22 | -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_unlock.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | 5 | *** Variables *** 6 | ${CAMUNDA_HOST} http://localhost:8080 7 | 8 | *** Test Cases *** 9 | Test unlock without having fetched anything 10 | unlock 11 | 12 | Test 'fetch and lock' for non existing topic 13 | [Setup] set camunda url ${CAMUNDA_HOST} 14 | # GIVEN 15 | ${non_existing_topic} Set Variable asdqeweasdwe 16 | 17 | # AND 18 | ${work_items} fetch workload topic=${non_existing_topic} 19 | 20 | # EXPECTED 21 | Should Be Empty ${work_items} 22 | 23 | # WHEN 24 | unlock -------------------------------------------------------------------------------- /tests/robot/test_camunda_url.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary ${CAMUNDA_URL} 3 | 4 | *** Variables *** 5 | ${CAMUNDA_URL} http://localhost:8080 6 | 7 | *** Test Case *** 8 | Camunda URL shall be normalized without trailing / (Issue #13) 9 | [Documentation] https://github.com/MarketSquare/robotframework-camunda/issues/13 10 | [Tags] issue-13 11 | ${current_camunda_url} Get Camunda Url 12 | Should be equal as Strings ${CAMUNDA_URL}/engine-rest ${current_camunda_url} 13 | 14 | Camunda URL shall be normalized with trailing / (Issue #13) 15 | [Documentation] https://github.com/MarketSquare/robotframework-camunda/issues/13 16 | [Tags] issue-13 17 | Set Camunda Url ${CAMUNDA_URL}/ 18 | ${current_camunda_url} Get Camunda Url 19 | Should be equal as Strings ${CAMUNDA_URL}/engine-rest ${current_camunda_url} 20 | 21 | -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_get_recent_process_instance.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | 5 | *** Variables *** 6 | ${CAMUNDA_HOST} http://localhost:8080 7 | 8 | *** Test Cases *** 9 | Never write recent process instance id when no workitem is fetched 10 | [Setup] set camunda url ${CAMUNDA_HOST} 11 | # GIVEN 12 | ${not_existing_topic} Set Variable asdfawesadas 13 | ${work_items} fetch workload topic=${not_existing_topic} 14 | 15 | # EXPECT 16 | Should be Empty ${work_items} Did not expect to receive work items for not existing topic:\t${not_existing_topic} 17 | 18 | # WHEN 19 | ${recent_task} get fetch response 20 | 21 | # THEN 22 | Should Be Empty ${recent_task} Should not have stored a recent task for none existing topic, but registered recent task id:\t${recent_task} 23 | 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Verwendet IntelliSense zum Ermitteln möglicher Attribute. 3 | // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. 4 | // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "robotframework-lsp", 9 | "name": "Robot Framework: Launch template", 10 | "request": "launch", 11 | "args": ["-V","tests/robot/config.py","-d","logs","-b","debug.log"] 12 | }, 13 | { 14 | "type": "robotframework-lsp", 15 | "name": "Robot Framework: Launch .robot file", 16 | "request": "launch", 17 | "cwd": "^\"\\${workspaceFolder}\"", 18 | "target": "^\"\\${file}\"", 19 | "terminal": "integrated", 20 | "env": {}, 21 | "args": ["-V","tests/robot/config.py","-d","logs","-b","debug.log"] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: [ push, pull_request ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ['3.9', '3.10','3.11','3.12'] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 26 | - name: Execute doctests 27 | run: | 28 | python CamundaLibrary/CamundaResources.py -v 29 | -------------------------------------------------------------------------------- /tests/robot/ProcessInstance/test_get_all_instances.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | 5 | 6 | *** Variables *** 7 | ${CAMUNDA_HOST} http://localhost:8080 8 | ${PROCESS_NAME} demo_for_robot 9 | 10 | 11 | *** Test Cases *** 12 | Get all instances 13 | # Given 14 | Upload process 15 | ${process_instances_before} get all active process instances ${PROCESS_NAME} 16 | 17 | # WHEN 18 | Start Process Instance ${PROCESS_NAME} 19 | ${process_instances_after} get all active process instances ${PROCESS_NAME} 20 | 21 | # THEN 22 | should be 1 more process ${process_instances_before} ${process_instances_after} 23 | 24 | 25 | *** Keywords *** 26 | Upload process 27 | deploy ${CURDIR}/../../bpmn/demo_for_robot.bpmn 28 | 29 | should be 1 more process 30 | [Arguments] ${before} ${after} 31 | ${count_before} Evaluate len($before)+1 32 | ${count_after} Evaluate len($after) 33 | Should be equal as integers ${count_before} ${count_after} -------------------------------------------------------------------------------- /tests/robot/ProcessDefinition/test_get_process_definitions.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | 5 | *** Variables *** 6 | ${CAMUNDA_HOST} http://localhost:8080 7 | ${PROCESS_DEFINITIONS} ${EMPTY} 8 | 9 | 10 | *** Test Cases *** 11 | Get All Process Definitions 12 | # Given 13 | At Least One Process Definition Is Present 14 | 15 | # When 16 | Camunda Is Requested For All Existing Process Definitions 17 | 18 | # Then 19 | Camunda Answered With A List Of At Least One Process Definition 20 | 21 | 22 | *** Keywords *** 23 | At Least One Process Definition Is Present 24 | Deploy ${CURDIR}/../../bpmn/demo_for_robot.bpmn 25 | 26 | Camunda Is Requested For All Existing Process Definitions 27 | ${PROCESS_DEFINITIONS} Get Process Definitions 28 | Log ${PROCESS_DEFINITIONS} 29 | Set Global Variable ${PROCESS_DEFINITIONS} 30 | 31 | Camunda Answered With A List Of At Least One Process Definition 32 | Should Be True len($PROCESS_DEFINITIONS) > 1 33 | ... msg=No Process Definitions found. 34 | -------------------------------------------------------------------------------- /libdoc/generate_libdoc.py: -------------------------------------------------------------------------------- 1 | from robot.libdoc import libdoc 2 | import os 3 | import re 4 | 5 | module_name = 'CamundaLibrary' 6 | 7 | # determine version number from environment 8 | version_regex = r"^v(?P\d*\.\d*\.\d*$)" 9 | version = os.environ.get('CI_COMMIT_TAG', f'1-{os.environ.get("CI_COMMIT_REF_NAME","dev")}') 10 | full_version_match = re.fullmatch(version_regex, version) 11 | if full_version_match: 12 | version = full_version_match.group('version') 13 | 14 | 15 | def generate_libdoc(): 16 | with os.scandir(f'./{module_name}') as dirs: 17 | for entry in dirs: 18 | filename, fileextenstion = os.path.splitext(entry.name) 19 | if '__init__' != filename and '.py' == fileextenstion: 20 | libdoc(f'{module_name}.{filename}', 21 | outfile=f'public/latest/keywords/{filename.lower()}/index.html', 22 | version=version) 23 | libdoc(f'{module_name}.{filename}', 24 | outfile=f'public/{version}/keywords/{filename.lower()}/index.html', 25 | version=version) 26 | 27 | 28 | if __name__ == '__main__': 29 | generate_libdoc() 30 | -------------------------------------------------------------------------------- /tests/robot/Deployment/test_deploy_bpmn.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Test Setup Set Camunda Configuration ${configuration} 4 | 5 | *** Test Cases *** 6 | Test deployment of models 7 | ${response} deploy ${CURDIR}/../../bpmn/demo_for_robot.bpmn 8 | Should Not Be Empty ${response} 9 | 10 | Test deployment of models and forms 11 | # WHEN 12 | ${response} deploy ${CURDIR}/../../form/embeddedSampleForm.html ${CURDIR}/../../bpmn/demo_for_robot.bpmn 13 | 14 | # THEN 15 | Should Not Be Empty ${response} 16 | 17 | # AND 18 | ${deployment} get deployments id=${response}[id] 19 | Should Not Be Empty ${deployment} No deployment found for id ${response}[id] 20 | 21 | Test error when deploying to incorrect url 22 | # GIVEN 23 | set camunda url http://localhost:6666 24 | 25 | # WHEN 26 | ${pass_message} ${error} Run Keyword and ignore error deploy ${CURDIR}/../../bpmn/demo_for_robot.bpmn 27 | 28 | # THEN 29 | Should Be Equal FAIL ${pass_message} 30 | Should contain ${error} ConnectionError 31 | [Teardown] set camunda url ${configuration}[host] -------------------------------------------------------------------------------- /tests/robot/ProcessInstance/test_delete_processes.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | 5 | *** Variables *** 6 | ${CAMUNDA_HOST} http://localhost:8080 7 | ${PROCESS_NAME} demo_for_robot 8 | 9 | 10 | *** Test Cases *** 11 | Get all instances 12 | # Given 13 | Upload process 14 | ${process_instances_before} get all active process instances ${PROCESS_NAME} 15 | Start Process Instance ${PROCESS_NAME} 16 | ${process_instances_after} get all active process instances ${PROCESS_NAME} 17 | 18 | # EXPECT 19 | should be 1 more process ${process_instances_before} ${process_instances_after} 20 | 21 | # WHEN 22 | FOR ${process_instance} IN @{process_instances_after} 23 | delete process instance ${process_instance}[id] 24 | END 25 | 26 | *** Keywords *** 27 | Upload process 28 | deploy ${CURDIR}/../../bpmn/demo_for_robot.bpmn 29 | 30 | should be 1 more process 31 | [Arguments] ${before} ${after} 32 | ${count_before} Evaluate len($before)+1 33 | ${count_after} Evaluate len($after) 34 | Should be equal as integers ${count_before} ${count_after} -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_bpmn_error.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Resource ../cleanup.resource 4 | Suite Setup Set Camunda Configuration ${configuration} 5 | Test Setup Delete all instances from process '${PROCESS_DEFINITION_KEY}' 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | ${PROCESS_DEFINITION_KEY} demo_for_robot 10 | ${EXISTING_TOPIC} process_demo_element 11 | 12 | *** Variables *** 13 | ${CAMUNDA_HOST} http://localhost:8080 14 | 15 | *** Test Cases *** 16 | BPMN error without task does not fail 17 | # only throws a warning 18 | throw bpmn error de1 19 | 20 | Test 'throw bpmn error' for existing topic 21 | [Setup] set camunda url ${CAMUNDA_HOST} 22 | # GIVEN 23 | Start Process Instance ${PROCESS_DEFINITION_KEY} 24 | ${variables} Create Dictionary text=Manna Manna 25 | 26 | # AND 27 | ${work_items} fetch workload topic=${existing_topic} 28 | 29 | # WHEN 30 | throw bpmn error de1 Alles kaputt variables=${variables} 31 | 32 | # THEN 33 | ${workload} Fetch Workload handle_error 34 | Should Not Be Empty ${workload} 35 | 36 | Should Be Equal As Strings Manna Manna ${workload}[text] 37 | Should Be Equal As Strings de1 ${workload}[error_code] 38 | Should Be Equal As Strings Alles kaputt ${workload}[error_message] 39 | -------------------------------------------------------------------------------- /.github/workflows/robot-test.yml: -------------------------------------------------------------------------------- 1 | name: Robot tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | integrationtest: 7 | runs-on: ubuntu-latest 8 | container: python:3.9 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | auth_enabled: ['false', 'true'] 13 | camunda_version: ['run-7.20.0'] 14 | services: 15 | camunda: 16 | image: camunda/camunda-bpm-platform:${{ matrix.camunda_version }} 17 | ports: 18 | - 8080:8080 19 | env: 20 | camunda.bpm.run.auth.enabled: ${{ matrix.auth_enabled }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Test with robot 24 | run: | 25 | pip install . 26 | sleep 10 27 | robot -d logs -b debug.log -x xunit.xml -L DEBUG -V tests/robot/config_cicd.py -v CAMUNDA_HOST:http://camunda:8080 tests/robot/**/*.robot 28 | - name: Archive production artifacts 29 | uses: actions/upload-artifact@v4 30 | if: always() 31 | with: 32 | name: robot logs-${{matrix.camunda_version}}-${{matrix.auth_enabled}} 33 | path: | 34 | logs 35 | - name: Download Artifacts 36 | uses: actions/download-artifact@v4 37 | with: 38 | name: robot logs-${{matrix.camunda_version}}-${{matrix.auth_enabled}} 39 | 40 | - name: Publish Unit Test Results 41 | uses: EnricoMi/publish-unit-test-result-action@v2 42 | with: 43 | files: xunit.xml 44 | -------------------------------------------------------------------------------- /tests/robot/ProcessInstance/test_get_activity_instance.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | Suite Teardown Clean Up Process Instance 5 | 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | ${PROCESS_INSTANCE_ID} ${EMPTY} 10 | ${ACTIVITY_INSTANCE_TREE} ${EMPTY} 11 | 12 | 13 | *** Test Cases *** 14 | Get Process Definitions 15 | # Given 16 | Process Instance Is Present 17 | 18 | # When 19 | Camunda Is Requested For Activity Instances Of The Process Instance 20 | 21 | # Then 22 | Camunda Answered With An Activity Instance Tree 23 | 24 | 25 | *** Keywords *** 26 | Process Instance Is Present 27 | ${response} Start Process Instance demo_for_robot 28 | Set Global Variable ${PROCESS_INSTANCE_ID} ${response}[id] 29 | 30 | Camunda Is Requested For Activity Instances Of The Process Instance 31 | ${ACTIVITY_INSTANCE_TREE} Get Activity Instance id=${PROCESS_INSTANCE_ID} 32 | Set Global Variable ${ACTIVITY_INSTANCE_TREE} 33 | 34 | Camunda Answered With An Activity Instance Tree 35 | Should Not Be Empty '${ACTIVITY_INSTANCE_TREE}' 36 | ${activity_instances} Set Variable ${ACTIVITY_INSTANCE_TREE}[child_activity_instances] 37 | Length Should Be ${activity_instances} 1 38 | Should Be Equal Activity_process_element ${activity_instances}[0][activity_id] 39 | 40 | Clean Up Process Instance 41 | Run Keyword If $PROCESS_INSTANCE_ID 42 | ... Delete Process Instance ${PROCESS_INSTANCE_ID} 43 | -------------------------------------------------------------------------------- /tests/robot/Decision/test_evaluate_decision.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | Suite Setup Set Camunda Configuration ${configuration} 5 | 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | ${DMN_KEY} demo_decision 10 | ${DECISIONS} ${EMPTY} 11 | 12 | 13 | *** Test Cases *** 14 | Decision Table Provides Correct Value 15 | # Given 16 | Decision DMD Was Deployed 17 | 18 | # When 19 | Decision DMD Is Requested 20 | 21 | # Then 22 | Decision Will Be Correct 23 | 24 | 25 | *** Keywords *** 26 | Decision DMD Was Deployed 27 | ${response} Deploy tests/bpmn/evaluate_decision.dmn 28 | Should Be True $response['id'] 29 | ... msg=Failed to deploy decision table. 30 | 31 | Decision DMD Is Requested 32 | ${DECISIONS} Create List 33 | Set Global Variable ${DECISIONS} 34 | Request Decision Max 20 ${True} 35 | Request Decision Bea 30 ${False} 36 | Request Decision Zen 99 ${False} 37 | 38 | Request Decision 39 | [Arguments] ${firstname} ${age} ${married} 40 | ${infos} Create Dictionary married=${married} 41 | ${variables} Create Dictionary 42 | ... firstname ${firstname} 43 | ... age ${age} 44 | ... infos ${infos} 45 | ${response} Evaluate Decision 46 | ... ${DMN_KEY} 47 | ... variables=${variables} 48 | Append To List ${DECISIONS} ${response} 49 | 50 | Decision Will Be Correct 51 | Log ${DECISIONS} 52 | Should Be Equal As Integers 3001001001 ${DECISIONS}[0][0][level] 53 | Should Be Equal As Integers 0 ${DECISIONS}[1][0][level] 54 | Should Be Equal As Integers -1 ${DECISIONS}[2][0][level] 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import re 5 | import os 6 | 7 | with open("README.md", "r") as fh: 8 | long_description = fh.read() 9 | 10 | name = "Markus Stahl" 11 | 12 | version_regex = r"^v(?P\d*\.\d*\.\d*$)" 13 | version = os.environ.get( 14 | "CI_COMMIT_TAG", f'2.{os.environ.get("CI_COMMIT_REF_NAME","0.0")}' 15 | ) 16 | full_version_match = re.fullmatch(version_regex, version) 17 | if full_version_match: 18 | version = full_version_match.group("version") 19 | 20 | setup( 21 | name="robotframework-camunda", 22 | version=version, 23 | description="Keywords for camunda rest api, leading open source workflow engine.", 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | author=name, 27 | author_email="markus.i.sverige@googlemail.com", 28 | url="https://github.com/MarketSquare/robotframework-camunda", 29 | packages=find_packages(), 30 | classifiers=[ 31 | "Intended Audience :: Developers", 32 | "Natural Language :: English", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Topic :: Software Development", 38 | "License :: OSI Approved :: Apache Software License", 39 | "Operating System :: OS Independent", 40 | "Development Status :: 5 - Production/Stable", 41 | "Framework :: Robot Framework", 42 | ], 43 | license="Apache License, Version 2.0", 44 | install_requires=[ 45 | "robotframework>=3.2", 46 | "requests", 47 | "frozendict", 48 | "generic-camunda-client>=7.15.0", 49 | "requests_toolbelt", 50 | "url-normalize", 51 | ], 52 | include_package_data=True, 53 | ) 54 | -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_get_amount_workloads.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | Resource ../cleanup.resource 5 | 6 | *** Variables *** 7 | ${CAMUNDA_HOST} http://localhost:8080 8 | ${PROCESS_DEFINITION_KEY} demo_for_robot 9 | ${TOPIC_NAME} process_demo_element 10 | 11 | *** Test Case *** 12 | There shall be as many tasks as started processes 13 | [Documentation] https://github.com/MarketSquare/robotframework-camunda/issues/6 14 | [Tags] issue-6 15 | [Template] Start Process Instancees and check amount of workloads 16 | 1 17 | 2 18 | 4 19 | 20 | There shall be as many tasks as started processes 21 | [Documentation] https://github.com/MarketSquare/robotframework-camunda/issues/6 22 | [Tags] issue-6 23 | [Template] Start Process Instance with business key and check for particular workload 24 | 1 25 | 2 26 | 4 27 | 28 | *** Keywords *** 29 | Start Process Instancees and check amount of workloads 30 | [Arguments] ${n} 31 | Delete all instances from process '${PROCESS_DEFINITION_KEY}' 32 | FOR ${i} IN RANGE 0 ${n} 33 | Start Process Instance ${PROCESS_DEFINITION_KEY} 34 | END 35 | 36 | ${amount_of_workloads} Get amount of workloads ${TOPIC_NAME} 37 | Should be equal as integers ${amount_of_workloads} ${n} 38 | 39 | Start Process Instance with business key and check for particular workload 40 | [Arguments] ${n} 41 | Delete all instances from process '${PROCESS_DEFINITION_KEY}' 42 | FOR ${i} IN RANGE 0 ${n} 43 | ${last_process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} business_key=${i} 44 | END 45 | 46 | ${amount_of_workloads} Get amount of workloads ${TOPIC_NAME} process_instance_id=${last_process_instance}[id] 47 | Should be equal as integers ${amount_of_workloads} 1 48 | 49 | -------------------------------------------------------------------------------- /tests/robot/ProcessInstance/test_get_process_instance_variable.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Suite Setup Set Camunda Configuration ${configuration} 4 | Suite Teardown Clean Up Process Instance 5 | 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | ${PROCESS_INSTANCE_ID} ${EMPTY} 10 | ${RESPONSE} ${EMPTY} 11 | ${value} 12 | 13 | *** Test Cases *** 14 | Get Process Instance Variable: String 15 | Given A value bar 16 | Given Process Instance Is Present 17 | When Camunda Is Requested For Variable Of The Process Instance 18 | Then Camunda Answered With Correct Value 19 | 20 | Get Process Instance Variable: List 21 | Given A List bar ber bir bor bur 22 | Given Process Instance Is Present 23 | When Camunda Is Requested For Variable Of The Process Instance 24 | Then Camunda Answered With Correct Value 25 | 26 | Get Process Instance Variable: Dictionary 27 | Given A List bar=1 ber=2 28 | Given Process Instance Is Present 29 | When Camunda Is Requested For Variable Of The Process Instance 30 | Then Camunda Answered With Correct Value 31 | 32 | *** Keywords *** 33 | A Dictionary 34 | [Arguments] &{values} 35 | Set Test Variable ${value} ${values} 36 | A List 37 | [Arguments] @{values} 38 | Set Test Variable ${value} ${values} 39 | 40 | A value 41 | [Arguments] ${testvalue} 42 | Set Test Variable ${value} ${testvalue} 43 | 44 | Process Instance Is Present 45 | ${variable} Create Dictionary foo=${value} 46 | ${response} Start Process Instance demo_for_robot variables=${variable} 47 | Set Global Variable ${PROCESS_INSTANCE_ID} ${response}[id] 48 | 49 | Camunda Is Requested For Variable Of The Process Instance 50 | ${RESPONSE} Get Process Instance Variable 51 | ... process_instance_id=${PROCESS_INSTANCE_ID} 52 | ... variable_name=foo 53 | Set Global Variable ${RESPONSE} 54 | 55 | Camunda Answered With Correct Value 56 | Should Be True $RESPONSE Variable could not be read or is empty. 57 | Should Be Equal ${RESPONSE} ${value} 58 | 59 | Clean Up Process Instance 60 | Run Keyword If $PROCESS_INSTANCE_ID 61 | ... Delete Process Instance ${PROCESS_INSTANCE_ID} 62 | -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_fetch_and_lock.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | Resource ../cleanup.resource 5 | Suite Setup Set Camunda Configuration ${configuration} 6 | Test Setup Delete all instances from process '${PROCESS_DEFINITION_KEY}' 7 | Test Teardown Reset CamundaLibrary 8 | 9 | *** Variables *** 10 | ${CAMUNDA_HOST} http://localhost:8080 11 | ${PROCESS_DEFINITION_KEY} demo_for_robot 12 | ${EXISTING_TOPIC} process_demo_element 13 | 14 | *** Test Cases *** 15 | Test 'fetch and lock' for existing topic 16 | #GIVEN 17 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${{{'variable_1': 1}}} 18 | #WHEN 19 | ${variables} fetch workload ${EXISTING_TOPIC} 20 | #THEN 21 | Should Not Be Empty ${variables} Fetching failed. Expected workload at topic '${EXISTING_TOPIC}' 22 | #AND 23 | Dictionary should contain key ${variables} variable_1 24 | 25 | Test 'fetch and lock' with only specific variables 26 | #GIVEN 27 | ${variable_name1} Set Variable variable1 28 | ${variable_name2} Set Variable variable2 29 | ${input_variables} Create Dictionary 30 | ... ${variable_name1}=1 31 | ... ${variable_name2}=2 32 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${input_variables} 33 | 34 | #WHEN 35 | ${variables} fetch workload ${EXISTING_TOPIC} variables=${{['${variable_name1}']}} 36 | 37 | #THEN 38 | Dictionary Should Contain key ${variables} ${variable_name1} 39 | Dictionary Should Not Contain Key ${variables} ${variable_name2} 40 | 41 | Test 'fetch and lock' for non existing topic 42 | # GIVEN 43 | ${non_existing_topic} Set Variable asdqeweasdwe 44 | 45 | # WHEN 46 | ${work_items} fetch workload topic=${non_existing_topic} 47 | 48 | # THEN 49 | Should Be Empty ${work_items} 50 | 51 | Test 'fetch and lock' for inacurrate camunda url 52 | # GIVEN 53 | ${invalid_camunda_url} Set Variable https://localhost:9212 54 | set camunda url ${invalid_camunda_url} 55 | 56 | # WHEN 57 | ${pass_message} ${error} Run Keyword and ignore error fetch workload topic=random 58 | 59 | # THEN 60 | Should Be Equal FAIL ${pass_message} 61 | Should contain ${error} ConnectionError 62 | 63 | *** Keywords *** 64 | Reset CamundaLibrary 65 | set camunda url ${CAMUNDA_HOST} 66 | complete task -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | # Camunda & Robot Framework 2 | *Introducing robotframework-camunda library* 3 | 4 | Resources: 5 | - [Source Code on GitHub.com](https://github.com/MarketSquare/robotframework-camunda) 6 | - [Example Test Cases](https://github.com/MarketSquare/robotframework-camunda/tree/master/tests) 7 | - [Issue Board](https://github.com/MarketSquare/robotframework-camunda/issues) 8 | 9 | **If you are Camunda user, you probably wonder: *What is Robot Framework?*** 10 | 11 | Robot Framework is an open source automation framework for task automation. Among 12 | other features, it offers a customizable domain language for developing tasks. 13 | It was originally developed for test automation providing the unique advantage 14 | making human readable test description executable. 15 | But most importantly: it comes with the enormous api universe from python. 16 | Meaning any python library can be integrated in to your task automation. 17 | 18 | **If you are Robot Framework user, you probably wonder: *What is Camunda?*** 19 | 20 | 2 sentences won't do it justice, just as the above summary about Robot Framework. 21 | In their words: Camunda is a collection of "open source workflow and decision 22 | automation tools [to] enable thousands of developers to automate business 23 | processes and gain the agility, visibility and scale that is needed to achieve 24 | digital transformation". 25 | 26 | As Robot Framework enables testers around the world automating their test cases 27 | by simply describing them, Camunda enables process designers to automate processes 28 | by describing in BPMN. You might summerize their power with the slogan: *Automation by documentation* 29 | Both might look like low code, but still offer the full power 30 | for developers to enhance their ecosystem. 31 | 32 | ## Integrating Camunda and Python 33 | 34 | Camunda is primarily built for Java developers, yet implemented the [External Task Pattern](https://docs.camunda.org/manual/7.14/user-guide/process-engine/external-tasks/#the-external-task-pattern) 35 | enabling developers from all programming languages integrating with Camunda through 36 | the very well documented [Camunda REST API](https://docs.camunda.org/manual/7.10/reference/rest/). 37 | For Robot Framework, all you need is implementing a few keywords for convenience 38 | wrapping requests to endpoints from Camunda REST API. Now you'd only need to 39 | package your library, publish it on pypi.org and there you go: the [robotframework-camunda](https://pypi.org/project/robotframework-camunda/) library. 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '39 1 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /tests/bpmn/evaluate_decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | firstname 8 | 9 | 10 | 11 | 12 | age 13 | 14 | 15 | 16 | 17 | infos.married 18 | 19 | 20 | 21 | 22 | 23 | "Max" 24 | 25 | 26 | 20 27 | 28 | 29 | true 30 | 31 | 32 | 3001001001 33 | 34 | 35 | 36 | 37 | "Bea" 38 | 39 | 40 | 30 41 | 42 | 43 | false 44 | 45 | 46 | 0 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -1 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # You can override the included template(s) by including variable overrides 2 | # See https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings 3 | # Note that environment variables can be set in several places 4 | # See https://docs.gitlab.com/ee/ci/variables/#priority-of-environment-variables 5 | stages: 6 | - build 7 | - test 8 | - deploy 9 | 10 | image: "$FEATURE_IMAGE_NAME:runtime" 11 | 12 | # scan for secret spoiling in project 13 | include: 14 | - template: Secret-Detection.gitlab-ci.yml 15 | - template: Security/SAST.gitlab-ci.yml 16 | 17 | variables: 18 | FEATURE_IMAGE_NAME: "$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG" 19 | DEPLOY_IMAGE_NAME: "$CI_REGISTRY_IMAGE" 20 | 21 | Build environment: 22 | stage: build 23 | image: docker:19.03.12 24 | services: 25 | - name: docker:dind 26 | variables: 27 | DOCKER_DRIVER: overlay2 28 | script: 29 | - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY 30 | - docker pull $DEPLOY_IMAGE_NAME/master:runtime || true 31 | - docker pull $FEATURE_IMAGE_NAME:runtime || true 32 | - | 33 | docker build \ 34 | --cache-from $FEATURE_IMAGE_NAME:runtime \ 35 | -t $FEATURE_IMAGE_NAME:runtime \ 36 | --target runtime \ 37 | . 38 | - docker push $FEATURE_IMAGE_NAME:runtime 39 | 40 | Unittests: 41 | stage: test 42 | before_script: 43 | - pip install -U unittest-xml-reporting 44 | script: 45 | - python CamundaLibrary/CamundaResources.py -v 46 | artifacts: 47 | expire_in: 1 week 48 | paths: 49 | - logs 50 | when: always 51 | reports: 52 | junit: logs/TEST*.xml 53 | 54 | Dryrun Robottests: 55 | stage: test 56 | before_script: 57 | - pip install . 58 | script: 59 | - robot -L DEBUG -b console -d output -x xunit.xml -V tests/robot/config_cicd.py -v CAMUNDA_HOST:http://camunda:8080 --dryrun tests/robot/**/*.robot 60 | artifacts: 61 | expire_in: 1 week 62 | paths: 63 | - output 64 | reports: 65 | junit: output/xunit.xml 66 | when: always 67 | 68 | Integrationtests: 69 | stage: test 70 | parallel: 71 | matrix: 72 | - CAMUNDA_VERSION: run-7.14.0 73 | - CAMUNDA_VERSION: run-7.15.0 74 | - CAMUNDA_VERSION: run-7.16.0 75 | services: 76 | - name: camunda/camunda-bpm-platform:${CAMUNDA_VERSION} 77 | alias: camunda 78 | before_script: 79 | - pip install . 80 | script: 81 | - robot -L DEBUG -b console -d output -x xunit.xml -V tests/robot/config_cicd.py -v CAMUNDA_HOST:http://camunda:8080 tests/robot/**/test*.robot 82 | artifacts: 83 | expire_in: 1 week 84 | paths: 85 | - output 86 | reports: 87 | junit: output/xunit.xml 88 | when: always 89 | 90 | 91 | ".package and deploy on testpypi": 92 | stage: deploy 93 | script: 94 | - python -m pip install --upgrade setuptools wheel twine 95 | - python setup.py sdist bdist_wheel 96 | - TWINE_PASSWORD=${TEST_PYPI_DEPLOYTOKEN} TWINE_USERNAME=__token__ python -m twine 97 | upload --verbose --repository testpypi dist/* 98 | only: 99 | - tags 100 | 101 | package and deploy on pypi: 102 | stage: deploy 103 | script: 104 | - python -m pip install --upgrade setuptools wheel twine 105 | - python setup.py sdist bdist_wheel 106 | - TWINE_PASSWORD=${PYPI_DEPLOYMENT_TOKEN} TWINE_USERNAME=__token__ python -m twine 107 | upload --verbose --repository pypi dist/* 108 | only: 109 | - tags 110 | 111 | pages: 112 | stage: deploy 113 | before_script: 114 | - pip install robotframework . 115 | script: 116 | - python libdoc/generate_libdoc.py 117 | artifacts: 118 | paths: 119 | - public 120 | only: 121 | - tags 122 | 123 | # do some code analysis 124 | sast: 125 | stage: test 126 | -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_dict_vars_to_json.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | Resource ../cleanup.resource 5 | Suite Setup Set Camunda Configuration ${configuration} 6 | Test Setup Delete all instances from process '${PROCESS_DEFINITION_KEY}' 7 | 8 | 9 | *** Variables *** 10 | ${CAMUNDA_HOST} http://localhost:8080 11 | ${PROCESS_DEFINITION_KEY} demo_for_robot 12 | ${EXISTING_TOPIC} process_demo_element 13 | 14 | 15 | *** Test Cases *** 16 | Dictionary variable remains dictionary 17 | # GIVEN 18 | ${variables} Process with dictionary variable 19 | 20 | # WHEN 21 | ${return_variables} Workload is fetched 22 | 23 | #THEN 24 | Should Not Be Empty ${return_variables} Fetching failed. Expected workload at topic '${EXISTING_TOPIC}' 25 | 26 | #AND 27 | Should Be Equal ${return_variables}[map][a] ${variables}[map][a] Dictionary value returned not as expected 28 | Should Be Equal ${return_variables}[map][b] ${variables}[map][b] Dictionary value returned not as expected 29 | 30 | 31 | Dictionary variable is of type JSON in camunda 32 | #GIVEN 33 | ${process_instance} Process with dictionary variable is started 34 | 35 | ${variable_instance} Get Process Instance Variable 36 | ... process_instance_id=${process_instance}[id] 37 | ... variable_name=map 38 | ... auto_type_conversion=${False} 39 | 40 | Should Be Equal Json ${variable_instance.type} Datatype for dictionary was supposed to be Json 41 | 42 | List variable remains list 43 | # GIVEN 44 | ${variables} Process with list variable 45 | 46 | # WHEN 47 | ${return_variables} Workload is fetched 48 | 49 | #THEN 50 | Should Not Be Empty ${return_variables} Fetching failed. Expected workload at topic '${EXISTING_TOPIC}' 51 | 52 | #AND 53 | Should Be Equal ${return_variables}[map][0] ${variables}[map][0] Dictionary value returned not as expected 54 | Should Be Equal ${return_variables}[map][1] ${variables}[map][1] Dictionary value returned not as expected 55 | 56 | 57 | List variable is of type JSON in camunda 58 | #GIVEN 59 | ${process_instance} Process with list variable is started 60 | 61 | ${variable_instance} Get Process Instance Variable 62 | ... process_instance_id=${process_instance}[id] 63 | ... variable_name=map 64 | ... auto_type_conversion=${False} 65 | 66 | Should Be Equal Json ${variable_instance.type} Datatype for dictionary was supposed to be Json 67 | 68 | 69 | *** Keywords *** 70 | Process with dictionary variable 71 | ${my_dict} Create Dictionary a=1 b=2 72 | ${variables} Create Dictionary map=${my_dict} 73 | ${process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} variables=${variables} 74 | [Return] ${variables} 75 | 76 | Process with list variable 77 | ${my_dict} Create list 1 2 78 | ${variables} Create Dictionary map=${my_dict} 79 | ${process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} variables=${variables} 80 | [Return] ${variables} 81 | 82 | Workload is fetched 83 | ${return_variables} fetch workload ${EXISTING_TOPIC} 84 | [Return] ${return_variables} 85 | 86 | Process with dictionary variable is started 87 | ${my_dict} Create Dictionary a=1 b=2 88 | ${variables} Create Dictionary map=${my_dict} 89 | ${process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} variables=${variables} 90 | [Return] ${process_instance} 91 | 92 | Process with list variable is started 93 | ${my_dict} Create Dictionary a=1 b=2 94 | ${variables} Create Dictionary map=${my_dict} 95 | ${process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} variables=${variables} 96 | [Return] ${process_instance} 97 | -------------------------------------------------------------------------------- /tests/robot/Message/deliver_message.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | Resource ../cleanup.resource 5 | Suite Setup Prepare Test Suite 6 | 7 | *** Variables *** 8 | ${MODEL} ${CURDIR}/../../bpmn/message_test.bpmn 9 | ${PROCESS_DEFINITION_KEY_SEND_MESSAGE} process_send_message 10 | ${PROCESS_DEFINITION_KEY_RECEIVE_MESSAGE} process_receive_message 11 | ${TOPIC_SEND_MESSAGE} send_message 12 | ${TOPIC_RECEIVE_MESSAGE} read_message 13 | ${MESSAGE_NAME} receive_message 14 | 15 | 16 | *** Test Cases *** 17 | Test Messaging with Message Result 18 | [Setup] Prepare testcase 19 | #GIVEN 20 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 21 | Last topic should have no workload 22 | ${workload} Get workload from topic '${TOPIC_SEND_MESSAGE}' 23 | 24 | # EXPECT 25 | Last topic should have workload 26 | 27 | # WHEN 28 | ${message_response} Deliver Message ${MESSAGE_NAME} 29 | Complete task 30 | 31 | # THEN 32 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 33 | Last topic should have workload 34 | Complete task 35 | 36 | Get workload from topic '${TOPIC_SEND_MESSAGE}' 37 | Last topic should have no workload 38 | 39 | Should Not Be Empty ${message_response} 40 | 41 | Test Messaging without Message Result 42 | [Setup] Prepare testcase 43 | #GIVEN 44 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 45 | Last topic should have no workload 46 | ${workload} Get workload from topic '${TOPIC_SEND_MESSAGE}' 47 | 48 | # EXPECT 49 | Last topic should have workload 50 | 51 | # WHEN 52 | ${message_response} Deliver Message ${MESSAGE_NAME} result_enabled=${False} 53 | Complete task 54 | 55 | # THEN 56 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 57 | Last topic should have workload 58 | Complete task 59 | 60 | Get workload from topic '${TOPIC_SEND_MESSAGE}' 61 | Last topic should have no workload 62 | 63 | Should Be Empty ${message_response} 64 | 65 | Test Messaging with variable 66 | [Setup] Prepare testcase 67 | #GIVEN 68 | ${process_variables} Create Dictionary message=Hello World 69 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 70 | Last topic should have no workload 71 | ${workload} Get workload from topic '${TOPIC_SEND_MESSAGE}' 72 | 73 | # EXPECT 74 | Last topic should have workload 75 | 76 | # WHEN 77 | ${message_response} Deliver Message ${MESSAGE_NAME} process_variables=${process_variables} 78 | Complete task 79 | 80 | # THEN 81 | ${received_message_workload} Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 82 | Last topic should have workload 83 | Dictionaries Should Be Equal ${process_variables} ${received_message_workload} 84 | Complete task 85 | 86 | Get workload from topic '${TOPIC_SEND_MESSAGE}' 87 | Last topic should have no workload 88 | 89 | Should Not Be Empty ${message_response} 90 | [Teardown] Complete Task 91 | 92 | Test Messaging with dict variable 93 | [Setup] Prepare testcase 94 | #GIVEN 95 | ${languages} Create List Suomi English 96 | ${person} Create Dictionary firstname=Pekka lastname=Klärck languages=${languages} 97 | ${process_variables} Create Dictionary person=${person} 98 | Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 99 | Last topic should have no workload 100 | ${workload} Get workload from topic '${TOPIC_SEND_MESSAGE}' 101 | 102 | # EXPECT 103 | Last topic should have workload 104 | 105 | # WHEN 106 | ${message_response} Deliver Message ${MESSAGE_NAME} process_variables=${process_variables} 107 | Complete task 108 | 109 | # THEN 110 | ${received_message_workload} Get workload from topic '${TOPIC_RECEIVE_MESSAGE}' 111 | Last topic should have workload 112 | Dictionaries Should Be Equal ${process_variables} ${received_message_workload} 113 | Complete task 114 | 115 | Get workload from topic '${TOPIC_SEND_MESSAGE}' 116 | Last topic should have no workload 117 | 118 | Should Not Be Empty ${message_response} 119 | 120 | 121 | *** Keywords *** 122 | Prepare Test Suite 123 | Set Camunda Configuration ${configuration} 124 | Deploy ${MODEL} 125 | 126 | Prepare testcase 127 | Delete all instances from process '${PROCESS_DEFINITION_KEY_SEND_MESSAGE}' 128 | Delete all instances from process '${PROCESS_DEFINITION_KEY_RECEIVE_MESSAGE}' 129 | Start Process Instance ${PROCESS_DEFINITION_KEY_SEND_MESSAGE} 130 | 131 | 132 | Get workload from topic '${topic}' 133 | ${workload} Fetch Workload ${topic} 134 | [Return] ${workload} 135 | 136 | Last topic should have workload 137 | ${recent_process_instance} Get fetch response 138 | Should not be empty ${recent_process_instance} 139 | 140 | Last topic should have no workload 141 | ${recent_process_instance} Get fetch response 142 | Should Be Empty ${recent_process_instance} -------------------------------------------------------------------------------- /tests/robot/ExternalTask/test_notify_failure.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Resource ../cleanup.resource 4 | Suite Setup Set Camunda Configuration ${configuration} 5 | Test Setup Delete all instances from process '${PROCESS_DEFINITION_KEY}' 6 | 7 | *** Variables *** 8 | ${CAMUNDA_HOST} http://localhost:8080 9 | ${PROCESS_DEFINITION_KEY} demo_for_robot 10 | ${EXISTING_TOPIC} process_demo_element 11 | 12 | 13 | *** Variables *** 14 | ${CAMUNDA_HOST} http://localhost:8080 15 | 16 | *** Test Cases *** 17 | Test 'Notify failure' for existing topic 18 | Given A new process instance 19 | ${process_instance} And process instance fetched 20 | 21 | When notify failure retry_timeout=${None} 22 | 23 | Then No Process Instance available at topic topic=${existing_topic} error_message=Notifying Failure failed. Process instance should not be available anymore at service task. 24 | And Process instance is incident ${process_instance} 25 | 26 | Test 'Notify failure' with setting retry_timeout 27 | # GIVEN 28 | Given A new process instance 29 | ${process_instance} And process instance fetched 30 | 31 | When notify failure retry_timeout=100 32 | 33 | Then No Process Instance available at topic topic=${existing_topic} error_message=Notifying Failure failed. Process instance should not be available anymore at service task. 34 | And Process instance is incident ${process_instance} 35 | 36 | Test 'Notify failure' with unfinished retries 37 | Given A new process instance 38 | ${process_instance} And Fetch Workload and notify failure retries=1 retry_timeout=0 39 | 40 | Then Process Instance available at topic topic=${existing_topic} error_message=Notifying Failure failed. Process instance should not be available anymore at service task. 41 | And Process instance is not incident ${process_instance} 42 | 43 | Test 'Notify failure' with retries countdown 44 | Given A new process instance 45 | ${process_instance} And Fetch Workload and notify failure retries=1 retry_timeout=0 46 | 47 | ${process_instance_retry} When Fetch Workload and notify failure retries=1 retry_timeout=0 48 | 49 | Then No Process Instance available at topic topic=${existing_topic} error_message=Notifying Failure failed. Process instance should not be available anymore at service task. 50 | And Process instance is incident ${process_instance_retry} 51 | 52 | *** Keywords *** 53 | A new process instance 54 | Start Process Instance ${PROCESS_DEFINITION_KEY} 55 | ${variables} Create Dictionary text=Manna Manna 56 | 57 | process instance fetched 58 | fetch workload topic=${existing_topic} lock_duration=500 59 | ${process_instance} Get fetch response 60 | log ${process_instance} 61 | Should Not be Empty ${process_instance} Failure while setting up test case. No process instance available to send failure for. 62 | [Return] ${process_instance} 63 | 64 | Fetch Workload and notify failure 65 | [Arguments] ${retries}=0 ${retry_timeout}=100 66 | ${process_instance} process instance fetched 67 | notify failure retries=${retries} retry_timeout=${retry_timeout} 68 | [Return] ${process_instance} 69 | 70 | No Process Instance available at topic 71 | [Arguments] ${topic} ${error_message}=None 72 | Sleep 1 s 73 | ${process_instance} Get process instance for topic ${topic} 74 | Should Be Empty ${process_instance} ${error_message} 75 | [Return] ${process_instance} 76 | 77 | Process Instance available at topic 78 | [Arguments] ${topic} ${error_message}=None 79 | Sleep 500 ms 80 | ${process_instance} Get process instance for topic ${topic} 81 | Should Not Be Empty ${process_instance} ${error_message} 82 | [Return] ${process_instance} 83 | 84 | Get process instance for topic 85 | [Arguments] ${topic} 86 | Fetch Workload topic=${topic} 87 | ${process_instance} Get fetch response 88 | [Return] ${process_instance} 89 | 90 | Process instance is incident 91 | [Arguments] ${process_instance} 92 | ${process_instance_after_failure} Get process instances process_instance_ids=${process_instance}[process_instance_id] 93 | log ${process_instance_after_failure} 94 | Should Not Be Empty ${process_instance_after_failure} Notifying Failure failed. Process instance is instance but should be an incident. 95 | 96 | ${incident} Get incidents process_instance_id=${process_instance}[process_instance_id] 97 | log ${incident} 98 | Should Not be Empty ${incident} Getting incident failed. There is no incident availabe matching the process instance. 99 | 100 | Process instance is not incident 101 | [Arguments] ${process_instance} 102 | ${incident} Get incidents process_instance_id=${process_instance}[process_instance_id] 103 | log ${incident} 104 | Should be Empty ${incident} Getting incident failed. There is no incident availabe matching the process instance. -------------------------------------------------------------------------------- /tests/bpmn/message_test.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Flow_0b7wd5l 11 | 12 | 13 | Flow_0d25nx3 14 | 15 | 16 | Flow_0b7wd5l 17 | Flow_0d25nx3 18 | 19 | 20 | 21 | 22 | 23 | 24 | Flow_1y1q6p6 25 | 26 | 27 | 28 | Flow_0h9hvs6 29 | 30 | 31 | Flow_1y1q6p6 32 | Flow_0h9hvs6 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /tests/bpmn/demo_for_robot.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flow_0xskn85 9 | 10 | 11 | Flow_0xskn85 12 | Flow_1m77rg0 13 | 14 | 15 | Flow_1m77rg0 16 | Flow_0thahmr 17 | 18 | 19 | Flow_0thahmr 20 | Flow_0uflgxn 21 | 22 | 23 | 24 | 25 | 26 | Flow_0jgdhq6 27 | 28 | 29 | 30 | 31 | 32 | Flow_0jgdhq6 33 | Flow_0uflgxn 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | markus.i.sverige + @ + googlemail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI status](https://img.shields.io/pypi/status/robotframework-camunda.svg)](https://pypi.python.org/pypi/robotframework-camunda/) [![pipeline status](https://gitlab.com/robotframework-camunda-demos/robotframework-camunda-mirror/badges/master/pipeline.svg)](https://gitlab.com/robotframework-camunda-demos/robotframework-camunda-mirror/-/commits/master) [![PyPi license](https://badgen.net/pypi/license/robotframework-camunda/)](https://pypi.com/project/robotframework-camunda/) [![PyPi version](https://badgen.net/pypi/v/robotframework-camunda/)](https://pypi.org/project/robotframework-camunda) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/robotframework-camunda.svg)](https://pypi.python.org/pypi/robotframework-camunda/) [![PyPI download month](https://img.shields.io/pypi/dm/robotframework-camunda.svg)](https://pypi.python.org/pypi/robotframework-camunda/) 2 | 3 | # Camunda 7 vs Camunda 8 4 | Since it requested from time o time: No, this library won't support Camunda 8. C8 and it's API are completely different and C8 API has had backwards incompatible changes due to its relatively low maturity. This library supports to when: 5 | - You migrate to Camunda 7 based workflow engine and you want to validate your processes are still working. 6 | 7 | If you need to test C8, it is recommended to build a separate library. 8 | 9 | # Robot Framework Camunda 10 | 11 | This library provides keywords for accessing camunda workflow engine. Complete REST API reference of camunda 12 | can be found [here](https://docs.camunda.org/manual/7.14/reference/rest/). 13 | 14 | **Please review [issue board](https://github.com/MarketSquare/robotframework-camunda/issues) for 15 | known issues or report one yourself. You are invited to contribute pull requests.** 16 | 17 | | Supported | Tested | 18 | | :----- | :----- | 19 | | Python >= 3.9 | 3.9, 3.10, 3.11, 3.12 | 20 | | Camunda 7 >= 7.20 | 7.20 | 21 | 22 | ## Documentation 23 | 24 | Keyword documentation is provided [here](https://robotframework-camunda-demos.gitlab.io/robotframework-camunda-mirror/latest/keywords/camundalibrary) 25 | 26 | ## Installation 27 | 28 | The library is published on [pypi.org](https://pypi.org/project/robotframework-camunda/) and can be installed with pip: 29 | 30 | ```shell 31 | pip install robotframework-camunda 32 | ``` 33 | 34 | ## Running robotframework-camunda 35 | The `tests` folder has example robot tests for keywords already implemented. Those tests assume you already have an 36 | instance of camunda running. 37 | 38 | Easiest way of running camunda is to launch with with docker: 39 | ```shell 40 | docker run -d --name camunda -p 8080:8080 camunda/camunda-bpm-platform:run-latest 41 | ``` 42 | 43 | ### Deploy process definition 44 | 45 | ```robot 46 | *** Settings *** 47 | Library CamundaLibrary ${CAMUNDA_HOST} 48 | 49 | *** Variables *** 50 | ${CAMUNDA_HOST} http://localhost:8080 51 | ${MODEL_FOLDER} ${CURDIR}/../models 52 | 53 | *** Test Cases *** 54 | Test deployment of a single model in 1 deployment 55 | ${response} deploy ${MODEL_FOLDER}/demo_for_robot.bpmn 56 | 57 | Test deployment of several models in 1 deployment 58 | ${response} deploy ${MODEL_FOLDER}/demo_for_robot.bpmn ${MODEL_FOLDER}/demo_embedded_form.html 59 | ``` 60 | 61 | ### Starting a process instance 62 | 63 | ```robot 64 | *** Settings *** 65 | Library CamundaLibrary ${CAMUNDA_HOST} 66 | 67 | *** Variables *** 68 | ${CAMUNDA_HOST} http://localhost:8080 69 | 70 | *** Test Cases *** 71 | Test starting process 72 | #GIVEN 73 | ${process_definition_key} Set Variable demo_for_robot 74 | 75 | # WHEN 76 | ${process_instance} start process ${process_definition_key} 77 | ``` 78 | 79 | ### Execute Task 80 | "Executing task" bascialy means, you execute a robot task that *fetches* a workload from camunda, processes it and 81 | returns its workload back to camunda during *completion*. Main keywords involved are: 82 | 1. `CamundaLibrary.Fetch workload` 83 | 1. `CamundaLibrary.Complete Task` 84 | 85 | ```robot 86 | *** Settings *** 87 | Library CamundaLibrary ${CAMUNDA_HOST} 88 | Library Collections 89 | 90 | *** Variables *** 91 | ${CAMUNDA_HOST} http://localhost:8000 92 | ${existing_topic} process_demo_element 93 | 94 | *** Test Cases *** 95 | Process workload 96 | ${variables} fetch workload topic=${existing_topic} 97 | ${recent_task} Get fetch response 98 | log Recent task:\t${recent_task} 99 | 100 | Pass Execution If not ${recent_task} No workload fetched from Camunda 101 | 102 | # do some processing 103 | 104 | # create result and return workload to Camunda 105 | ${my_result} Create Dictionary lastname=Stahl 106 | complete task ${my_result} 107 | ``` 108 | 109 | ### Authentication 110 | 111 | **Prerequisite: CamundaLibrary >= 2.0** 112 | 113 | If your Camunda Platform REST API requires authentication (it should at least in production!) then you do not need to pass the host url to CamundaLibrary during intialization. You require the `Set Camunda Configuration` keyword. The keyword expects a dictionary with host url and (optional) either username with password or api key with optional api key prefix. See the following example. 114 | 115 | ```robot 116 | *** Settings *** 117 | Library CamundaLibrary 118 | 119 | 120 | *** Test Cases *** 121 | Demonstrate basic auth 122 | ${camunda_config} Create Dictionary host=http://localhost:8080 username=markus password=%{ENV_PASSWORD} 123 | Set Camunda Configuration ${camunda_config} 124 | ${deployments} Get deployments #uses basic auth now implictly 125 | 126 | Demonstrate Api Key 127 | ${camunda_config} Create Dictionary host=http://localhost:8080 api_key=%{ENV_API_KEY} api_key_prefix=Bearer 128 | Set Camunda Configuration ${camunda_config} 129 | ${deployments} Get deployments #uses api key implicitly 130 | ``` 131 | If you would pass in username+password *and* and API key, the API key will always be chosen over the username+password. So better leave it out for not confusing everybody. 132 | -------------------------------------------------------------------------------- /CamundaLibrary/CamundaResources.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import os 4 | from collections.abc import Collection 5 | from typing import Dict, Any 6 | 7 | from generic_camunda_client import Configuration, ApiClient, VariableValueDto 8 | 9 | 10 | class CamundaResources: 11 | """ 12 | Singleton containing resources shared by Camunda sub libraries 13 | """ 14 | 15 | _instance = None 16 | 17 | _client_configuration: Configuration = None 18 | 19 | _api_client: ApiClient = None 20 | 21 | def __new__(cls): 22 | if cls._instance is None: 23 | print("Creating the object") 24 | cls._instance = super(CamundaResources, cls).__new__(cls) 25 | # Put any initialization here. 26 | return cls._instance 27 | 28 | # cammunda_url parameter is only required as long not every keyword uses generic-camunda-client 29 | @property 30 | def camunda_url(self) -> str: 31 | return self.client_configuration.host 32 | 33 | @camunda_url.setter 34 | def camunda_url(self, value: str): 35 | if not self._client_configuration: 36 | self.client_configuration = Configuration(host=value) 37 | else: 38 | self.client_configuration.host = value 39 | 40 | @property 41 | def client_configuration(self) -> Configuration: 42 | return self._client_configuration 43 | 44 | @client_configuration.setter 45 | def client_configuration(self, value): 46 | self._client_configuration = value 47 | if self._api_client: 48 | self._api_client = self._create_task_client() 49 | 50 | @property 51 | def api_client(self) -> ApiClient: 52 | if not self._api_client: 53 | self._api_client = self._create_task_client() 54 | return self._api_client 55 | 56 | def _create_task_client(self) -> ApiClient: 57 | if not self.client_configuration: 58 | raise ValueError( 59 | "No URL to camunda set. Please initialize Library with url or use keyword " 60 | '"Set Camunda URL" first.' 61 | ) 62 | 63 | # the generated client for camunda ignores auth parameters from the configuration. Therefore we must set default headers here: 64 | client = ApiClient(self.client_configuration) 65 | if self.client_configuration.username: 66 | client.set_default_header( 67 | "Authorization", self.client_configuration.get_basic_auth_token() 68 | ) 69 | elif self.client_configuration.api_key: 70 | identifier = list(self.client_configuration.api_key.keys())[0] 71 | client.set_default_header( 72 | "Authorization", 73 | self.client_configuration.get_api_key_with_prefix(identifier), 74 | ) 75 | 76 | return client 77 | 78 | @staticmethod 79 | def convert_openapi_variables_to_dict( 80 | open_api_variables: Dict[str, VariableValueDto] 81 | ) -> Dict: 82 | """ 83 | Converts the variables to a simple dictionary 84 | :return: dict 85 | {"var1": {"value": 1}, "var2": {"value": True}} 86 | -> 87 | {"var1": 1, "var2": True} 88 | """ 89 | if not open_api_variables: 90 | return {} 91 | return { 92 | k: CamundaResources.convert_variable_dto(v) 93 | for k, v in open_api_variables.items() 94 | } 95 | 96 | @staticmethod 97 | def convert_dict_to_openapi_variables( 98 | variabes: dict, 99 | ) -> Dict[str, VariableValueDto]: 100 | """ 101 | Converts the variables to a simple dictionary 102 | :return: dict 103 | {"var1": 1, "var2": True} 104 | -> 105 | {'var1': {'type': None, 'value': 1, 'value_info': None}, 'var2': {'type': None, 'value': True, 'value_info': None}} 106 | 107 | Example: 108 | >>> CamundaResources.convert_dict_to_openapi_variables({"var1": 1, "var2": True}) 109 | {'var1': {'type': None, 'value': 1, 'value_info': None}, 'var2': {'type': None, 'value': True, 'value_info': None}} 110 | 111 | >>> CamundaResources.convert_dict_to_openapi_variables({}) 112 | {} 113 | """ 114 | if not variabes: 115 | return {} 116 | return { 117 | k: CamundaResources.convert_to_variable_dto(v) for k, v in variabes.items() 118 | } 119 | 120 | @staticmethod 121 | def convert_file_dict_to_openapi_variables( 122 | files: Dict[str, str] 123 | ) -> Dict[str, VariableValueDto]: 124 | """ 125 | Example: 126 | >>> CamundaResources.convert_file_dict_to_openapi_variables({'testfile': 'tests/resources/test.txt'}) 127 | {'testfile': {'type': 'File', 128 | 'value': 'VGhpcyBpcyBhIHRlc3QgZmlsZSBmb3IgYSBjYW11bmRhIHByb2Nlc3Mu', 129 | 'value_info': {'filename': 'test.txt', 'mimetype': 'text/plain'}}} 130 | 131 | >>> CamundaResources.convert_file_dict_to_openapi_variables({}) 132 | {} 133 | """ 134 | if not files: 135 | return {} 136 | return {k: CamundaResources.convert_file_to_dto(v) for (k, v) in files.items()} 137 | 138 | @staticmethod 139 | def convert_file_to_dto(path: str) -> VariableValueDto: 140 | if not path: 141 | raise FileNotFoundError( 142 | "Cannot create DTO from file, because no file provided" 143 | ) 144 | 145 | with open(path, "r+b") as file: 146 | file_content = base64.standard_b64encode(file.read()).decode("utf-8") 147 | 148 | base = os.path.basename(path) 149 | file_name, file_ext = os.path.splitext(base) 150 | 151 | if file_ext.lower() in [".jpg", ".jpeg", ".jpe"]: 152 | mimetype = "image/jpeg" 153 | elif file_ext.lower() in [".png"]: 154 | mimetype = "image/png" 155 | elif file_ext.lower() in [".pdf"]: 156 | mimetype = "application/pdf" 157 | elif file_ext.lower() in [".txt"]: 158 | mimetype = "text/plain" 159 | else: 160 | mimetype = "application/octet-stream" 161 | return VariableValueDto( 162 | value=file_content, 163 | type="File", 164 | value_info={"filename": base, "mimetype": mimetype}, 165 | ) 166 | 167 | @staticmethod 168 | def convert_to_variable_dto(value: Any) -> VariableValueDto: 169 | if isinstance(value, str): 170 | return VariableValueDto(value=value) 171 | elif isinstance( 172 | value, Collection 173 | ): # String is also a collection and must be filtered before Collection. 174 | return VariableValueDto(value=json.dumps(value), type="Json") 175 | else: 176 | return VariableValueDto(value=value) 177 | 178 | @staticmethod 179 | def convert_variable_dto(dto: VariableValueDto) -> Any: 180 | if dto.type == "File": 181 | return dto.to_dict() 182 | if dto.type == "Json": 183 | return json.loads(dto.value) 184 | return dto.value 185 | 186 | @staticmethod 187 | def dict_to_camunda_json(d: dict) -> Any: 188 | """ 189 | Example: 190 | >>> CamundaResources.dict_to_camunda_json({'a':1}) 191 | {'a': {'value': 1}} 192 | 193 | >>> CamundaResources.dict_to_camunda_json({'person': {'age': 25}}) 194 | {'person': {'value': '{"age": 25}', 'type': 'Json'}} 195 | 196 | >>> CamundaResources.dict_to_camunda_json({'languages': ['English', 'Suomi']}) 197 | {'languages': {'value': '["English", "Suomi"]', 'type': 'Json'}} 198 | 199 | >>> CamundaResources.dict_to_camunda_json({'person': {'age': 25, 'languages': ['English', 'Suomi']}}) 200 | {'person': {'value': '{"age": 25, "languages": ["English", "Suomi"]}', 'type': 'Json'}} 201 | """ 202 | return { 203 | k: ( 204 | {"value": json.dumps(v), "type": "Json"} 205 | if isinstance(v, Collection) 206 | else {"value": v} 207 | ) 208 | for k, v in d.items() 209 | } 210 | 211 | 212 | if __name__ == "__main__": 213 | import doctest 214 | 215 | doctest.testmod() 216 | -------------------------------------------------------------------------------- /tests/robot/Execution/test_start_process.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library CamundaLibrary 3 | Library Collections 4 | Library OperatingSystem 5 | Resource ../cleanup.resource 6 | Suite Setup Set Camunda Configuration ${configuration} 7 | Test Setup Delete all instances from process '${PROCESS_DEFINITION_KEY}' 8 | 9 | 10 | *** Variables *** 11 | ${CAMUNDA_HOST} http://localhost:8080 12 | ${PROCESS_DEFINITION_KEY} demo_for_robot 13 | 14 | *** Test Cases *** 15 | Test starting process 16 | # WHEN 17 | ${process_instance} Start Process Instance ${PROCESS_DEFINITION_KEY} 18 | 19 | [Teardown] delete process instance ${process_instance}[id] 20 | 21 | Test starting process with variables 22 | #GIVEN 23 | ${PROCESS_DEFINITION_KEY} Set Variable demo_for_robot 24 | ${existing_topic} Set Variable process_demo_element 25 | 26 | ${variable1_value} Set Variable test1 27 | ${variable1_key} Set Variable my_value 28 | ${variables} Create Dictionary ${variable1_key}=${variable1_value} 29 | 30 | # WHEN 31 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${variables} 32 | 33 | # AND 34 | ${first_workload} fetch workload topic=${existing_topic} 35 | 36 | # THEN 37 | Should Not be empty ${first_workload} 38 | 39 | # AND 40 | dictionary should contain key ${first_workload} ${variable1_key} 41 | Should Not be empty ${first_workload}[${variable1_key}] 42 | Should Be Equal ${variable1_value} ${first_workload}[${variable1_key}] 43 | [Teardown] complete task 44 | 45 | Test starting process with business key 46 | #GIVEN 47 | ${PROCESS_DEFINITION_KEY} Set Variable demo_for_robot 48 | ${existing_topic} Set Variable process_demo_element 49 | 50 | ${expected_business_key} Set Variable business 1 51 | 52 | # WHEN 53 | Start Process Instance ${PROCESS_DEFINITION_KEY} business_key=${expected_business_key} 54 | 55 | # AND 56 | ${first_workload} fetch workload topic=${existing_topic} async_response_timeout=100 57 | ${response} get fetch response 58 | 59 | # THEN 60 | Should Not be empty ${response} 61 | 62 | # AND 63 | dictionary should contain key ${response} business_key 64 | Should Not be empty ${response}[business_key] 65 | Should Be Equal ${expected_business_key} ${response}[business_key] 66 | [Teardown] complete task 67 | 68 | Test starting process with variables after activity 69 | #GIVEN 70 | ${existing_topic} Set Variable process_demo_element 71 | 72 | ${variable1_value} Set Variable After activity test 73 | ${variable1_key} Set Variable my_value 74 | ${variables} Create Dictionary ${variable1_key}=${variable1_value} 75 | 76 | ${after_activity_id} Set Variable Activity_process_element 77 | 78 | # WHEN 79 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${variables} after_activity_id=${after_activity_id} 80 | 81 | # AND 82 | ${first_workload} fetch workload topic=${existing_topic} 83 | 84 | # THEN 85 | Should be empty ${first_workload} 86 | 87 | [Teardown] complete task 88 | 89 | Test starting process with variables before activity 90 | #GIVEN 91 | ${existing_topic} Set Variable process_demo_element 92 | 93 | ${variable1_value} Set Variable Before activity test 94 | ${variable1_key} Set Variable my_value 95 | ${variables} Create Dictionary ${variable1_key}=${variable1_value} 96 | 97 | ${before_activity_id} Set Variable Activity_process_element 98 | 99 | # WHEN 100 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${variables} before_activity_id=${before_activity_id} 101 | 102 | # AND 103 | ${first_workload} fetch workload topic=${existing_topic} 104 | 105 | # THEN 106 | Should Not be empty ${first_workload} 107 | 108 | # AND 109 | dictionary should contain key ${first_workload} ${variable1_key} 110 | Should Not be empty ${first_workload}[${variable1_key}] 111 | Should Be Equal ${variable1_value} ${first_workload}[${variable1_key}] 112 | [Teardown] complete task 113 | 114 | Test starting process with dict variables 115 | #GIVEN 116 | ${existing_topic} Set Variable process_demo_element 117 | 118 | ${variable1_value} Set Variable test1 119 | ${variable1_key} Set Variable my_value 120 | ${variables1} Create Dictionary ${variable1_key}=${variable1_value} 121 | ${variables} Create Dictionary variables1=${variables1} 122 | 123 | # WHEN 124 | Start Process Instance ${PROCESS_DEFINITION_KEY} ${variables} 125 | 126 | # AND 127 | ${first_workload} fetch workload topic=${existing_topic} 128 | 129 | # THEN 130 | Should Not be empty ${first_workload} 131 | 132 | # AND 133 | dictionary should contain key ${first_workload} variables1 134 | dictionary should contain key ${first_workload}[variables1] ${variable1_key} 135 | Should Not be empty ${first_workload}[variables1][${variable1_key}] 136 | Should Be Equal ${variable1_value} ${first_workload}[variables1][${variable1_key}] 137 | [Teardown] complete task 138 | 139 | Test starting process with file variables 140 | #GIVEN 141 | ${existing_topic} Set Variable process_demo_element 142 | 143 | ${files} Create Dictionary my_file=tests/resources/rf-logo.png 144 | 145 | # WHEN 146 | Start Process Instance ${PROCESS_DEFINITION_KEY} files=${files} 147 | 148 | # AND 149 | ${first_workload} fetch workload topic=${existing_topic} 150 | 151 | # THEN 152 | Should Not be empty ${first_workload} 153 | 154 | # AND 155 | dictionary should contain key ${first_workload} my_file 156 | 157 | # AND 158 | Should not be empty ${first_workload}[my_file] 159 | ${file} Download file from variable my_file 160 | [Teardown] complete task 161 | 162 | Test file content from starting process variable 163 | #GIVEN 164 | ${existing_topic} Set Variable process_demo_element 165 | ${testfile} Set Variable tests/resources/test.txt 166 | ${testfile_content} Get File ${testfile} 167 | 168 | ${files} Create Dictionary my_file=${testfile} 169 | 170 | # WHEN 171 | Start Process Instance ${PROCESS_DEFINITION_KEY} files=${files} 172 | 173 | # AND 174 | ${first_workload} fetch workload topic=${existing_topic} 175 | 176 | # THEN 177 | Should Not be empty ${first_workload} 178 | 179 | # AND 180 | dictionary should contain key ${first_workload} my_file 181 | 182 | # AND 183 | Should not be empty ${first_workload}[my_file] 184 | ${process_file} Download file from variable my_file 185 | ${process_file_content} Get File ${process_file} 186 | Should be equal as Strings ${testfile_content} ${process_file_content} 187 | [Teardown] complete task 188 | 189 | Test starting process with file variables 190 | #GIVEN 191 | ${process_definition_key} Set Variable demo_for_robot 192 | ${existing_topic} Set Variable process_demo_element 193 | 194 | ${files} Create Dictionary my_file=tests/resources/rf-logo.png 195 | 196 | # WHEN 197 | Start Process Instance ${process_definition_key} files=${files} 198 | 199 | # AND 200 | ${first_workload} fetch workload topic=${existing_topic} 201 | 202 | # THEN 203 | Should Not be empty ${first_workload} 204 | 205 | # AND 206 | dictionary should contain key ${first_workload} my_file 207 | 208 | # AND 209 | Should not be empty ${first_workload}[my_file] 210 | ${file} Download file from variable my_file 211 | [Teardown] complete task 212 | 213 | Test file content from starting process variable 214 | #GIVEN 215 | ${process_definition_key} Set Variable demo_for_robot 216 | ${existing_topic} Set Variable process_demo_element 217 | ${testfile} Set Variable tests/resources/test.txt 218 | ${testfile_content} Get File ${testfile} 219 | 220 | ${files} Create Dictionary my_file=${testfile} 221 | 222 | # WHEN 223 | Start Process Instance ${process_definition_key} files=${files} 224 | 225 | # AND 226 | ${first_workload} fetch workload topic=${existing_topic} 227 | 228 | # THEN 229 | Should Not be empty ${first_workload} 230 | 231 | # AND 232 | dictionary should contain key ${first_workload} my_file 233 | 234 | # AND 235 | Should not be empty ${first_workload}[my_file] 236 | ${process_file} Download file from variable my_file 237 | ${process_file_content} Get File ${process_file} 238 | Should be equal as Strings ${testfile_content} ${process_file_content} 239 | [Teardown] complete task 240 | 241 | Test starting process with file variables 242 | #GIVEN 243 | ${process_definition_key} Set Variable demo_for_robot 244 | ${existing_topic} Set Variable process_demo_element 245 | 246 | ${files} Create Dictionary my_file=tests/resources/rf-logo.png 247 | 248 | # WHEN 249 | Start Process Instance ${process_definition_key} files=${files} 250 | 251 | # AND 252 | ${first_workload} fetch workload topic=${existing_topic} 253 | 254 | # THEN 255 | Should Not be empty ${first_workload} 256 | 257 | # AND 258 | dictionary should contain key ${first_workload} my_file 259 | 260 | # AND 261 | Should not be empty ${first_workload}[my_file] 262 | ${file} Download file from variable my_file 263 | [Teardown] complete task 264 | 265 | Test file content from starting process variable 266 | #GIVEN 267 | ${process_definition_key} Set Variable demo_for_robot 268 | ${existing_topic} Set Variable process_demo_element 269 | ${testfile} Set Variable tests/resources/test.txt 270 | ${testfile_content} Get File ${testfile} 271 | 272 | ${files} Create Dictionary my_file=${testfile} 273 | 274 | # WHEN 275 | Start Process Instance ${process_definition_key} files=${files} 276 | 277 | # AND 278 | ${first_workload} fetch workload topic=${existing_topic} 279 | 280 | # THEN 281 | Should Not be empty ${first_workload} 282 | 283 | # AND 284 | dictionary should contain key ${first_workload} my_file 285 | 286 | # AND 287 | Should not be empty ${first_workload}[my_file] 288 | ${process_file} Download file from variable my_file 289 | ${process_file_content} Get File ${process_file} 290 | Should be equal as Strings ${testfile_content} ${process_file_content} 291 | [Teardown] complete task 292 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2022 MarketSquare 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /CamundaLibrary/CamundaLibrary.py: -------------------------------------------------------------------------------- 1 | # robot imports 2 | 3 | from generic_camunda_client.configuration import Configuration 4 | from robot.api.deco import library, keyword 5 | from robot.api.logger import librarylogger as logger 6 | 7 | # requests import 8 | from requests import HTTPError 9 | import requests 10 | from requests_toolbelt.multipart.encoder import MultipartEncoder 11 | 12 | from url_normalize import url_normalize 13 | 14 | # python imports 15 | import os 16 | from typing import List, Dict, Any 17 | import time 18 | 19 | from generic_camunda_client import ( 20 | ApiException, 21 | CountResultDto, 22 | DeploymentWithDefinitionsDto, 23 | DeploymentDto, 24 | LockedExternalTaskDto, 25 | VariableValueDto, 26 | FetchExternalTasksDto, 27 | FetchExternalTaskTopicDto, 28 | ProcessDefinitionApi, 29 | ProcessInstanceWithVariablesDto, 30 | StartProcessInstanceDto, 31 | ProcessInstanceModificationInstructionDto, 32 | ProcessInstanceApi, 33 | ProcessInstanceDto, 34 | VersionApi, 35 | EvaluateDecisionDto, 36 | MessageApi, 37 | MessageCorrelationResultWithVariableDto, 38 | CorrelationMessageDto, 39 | ActivityInstanceDto, 40 | ExternalTaskFailureDto, 41 | IncidentApi, 42 | IncidentDto, 43 | ) 44 | 45 | import generic_camunda_client as openapi_client 46 | 47 | # local imports 48 | from .CamundaResources import CamundaResources 49 | 50 | 51 | @library(scope="SUITE") 52 | class CamundaLibrary: 53 | """ 54 | Library for Camunda integration in Robot Framework 55 | 56 | = Installation = 57 | 58 | == Camunda == 59 | 60 | Easiest to run Camunda in docker: 61 | | docker run -d --name camunda -p 8080:8080 camunda/camunda-bpm-platform:run-latest 62 | 63 | == CamundaLibrary == 64 | You can use pip for installing CamundaLibrary: 65 | | pip install robotframework-camunda 66 | 67 | = Usage = 68 | 69 | The library provides convenience keywords for accessing Camunda via REST API. You may deploy models, 70 | start processes, fetch workloads and complete them. 71 | 72 | When initializing the library the default url for Camunda is `http://localhost:8080` which is the default when 73 | running Camunda locally. It is best practice to provide a variable for the url, so it can be set dynamically 74 | by the executing environment (like on local machine, in pipeline, on test system and production): 75 | 76 | | Library | CamundaLibrary | ${CAMUNDA_URL} | 77 | 78 | Running locally: 79 | | robot -v "CAMUNDA_URL:http://localhost:8080" task.robot 80 | 81 | Running in production 82 | | robot -v "CAMUNDA_URL:https://camunda-prod.mycompany.io" task.robot 83 | 84 | = Execution = 85 | 86 | In contrast to external workers that are common for Camunda, tasks implemented with CamundaLibrary do not 87 | _subscribe_ on certain topics. A robot tasks is supposed to run once. How frequent the task is executed is up 88 | to the operating environment of the developer. 89 | 90 | == Subscribing a topic / long polling == 91 | You may achieve a kind of subscription by providing the ``asyncResponseTimeout`` with the `Fetch workload` 92 | keyword in order to achieve [https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/#long-polling-to-fetch-and-lock-external-tasks|Long Polling]. 93 | 94 | | ${variables} | fetch workload | my_topic | async_response_timeout=60000 | 95 | | log | Waited at most 1 minute before this log statement got executed | 96 | 97 | = Missing Keywords = 98 | 99 | If you miss a keyword, you can utilize the REST API from Camunda by yourself using the [https://github.com/MarketSquare/robotframework-requests|RequestsLibrary]. 100 | With RequestsLibrary you can access all of the fully documented [https://docs.camunda.org/manual/latest/reference/rest/|Camunda REST API]. 101 | 102 | = Feedback = 103 | 104 | Feedback is very much appreciated regardless if it is comments, reported issues, feature requests or even merge 105 | requests. You are welcome to participating in any way at the [https://github.com/MarketSquare/robotframework-camunda|GitHub project of CamundaLibrary]. 106 | """ 107 | 108 | WORKER_ID = f"robotframework-camundalibrary-{time.time()}" 109 | 110 | EMPTY_STRING = "" 111 | KNOWN_TOPICS: Dict[str, Dict[str, Any]] = {} 112 | FETCH_RESPONSE: LockedExternalTaskDto = {} 113 | DEFAULT_LOCK_DURATION = None 114 | 115 | def __init__(self, host="http://localhost:8080"): 116 | self._shared_resources = CamundaResources() 117 | self.set_camunda_configuration(configuration={"host": host}) 118 | self.DEFAULT_LOCK_DURATION = self.reset_task_lock_duration() 119 | 120 | @keyword(tags=["configuration"]) 121 | def set_camunda_configuration(self, configuration: dict): 122 | if "host" not in configuration.keys(): 123 | raise ValueError( 124 | f"Incomplete configuration. Configuration must include at least the Camunda host url:\t{configuration}" 125 | ) 126 | 127 | # weird things happen when dictionary is not copied and keyword is called repeatedly. Somehow robot or python remember the configuration from the previous call 128 | camunda_config = configuration.copy() 129 | 130 | host = configuration["host"] 131 | camunda_config["host"] = url_normalize(f"{host}/engine-rest") 132 | 133 | if "api_key" in configuration.keys(): 134 | api_key = configuration["api_key"] 135 | camunda_config["api_key"] = {"default": api_key} 136 | if "api_key_prefix" in configuration.keys(): 137 | api_key_prefix = configuration["api_key_prefix"] 138 | camunda_config["api_key_prefix"] = {"default": api_key_prefix} 139 | 140 | logger.debug(f"New configuration for Camunda client:\t{camunda_config}") 141 | self._shared_resources.client_configuration = Configuration(**camunda_config) 142 | 143 | @keyword(tags=["configuration"]) 144 | def set_camunda_url(self, url: str): 145 | """ 146 | Sets url for camunda eninge. Only necessary when URL cannot be set during initialization of this library or 147 | you want to switch camunda url for some reason. 148 | """ 149 | if not url: 150 | raise ValueError("Cannot set camunda engine url: no url given.") 151 | self._shared_resources.camunda_url = url_normalize(f"{url}/engine-rest") 152 | 153 | @keyword(tags=["task", "configuration"]) 154 | def set_task_lock_duration(self, lock_duration: int): 155 | """ 156 | Sets lock duration used as default when fetching. Camunda locks a process instance for the period. If the 157 | external task os not completed, Camunda gives the process instance free for another attempt only when lock 158 | duration has expired. 159 | 160 | Value is in milliseconds (1000 = 1 minute) 161 | """ 162 | try: 163 | self.DEFAULT_LOCK_DURATION = int(lock_duration) 164 | except ValueError: 165 | logger.error( 166 | f"Failed to set lock duration. Value does not seem a valid integer:\t{lock_duration}" 167 | ) 168 | 169 | @keyword(tags=["task", "configuration"]) 170 | def reset_task_lock_duration(self): 171 | """ 172 | Counter keyword for "Set Task Lock Duration". Resets lock duration to the default. The default is either 173 | environment variable CAMUNDA_TASK_LOCK_DURATION or 600000 (10 minutes). 174 | """ 175 | try: 176 | lock_duration = int(os.environ.get("CAMUNDA_TASK_LOCK_DURATION", 600000)) 177 | except ValueError as e: 178 | logger.warn( 179 | f'Failed to interpret "CAMUNDA_TASK_LOCK_DURATION". Environment variable does not seem to contain a valid integer:\t{e}' 180 | ) 181 | lock_duration = 600000 182 | return lock_duration 183 | 184 | @keyword(tags=["configuration"]) 185 | def get_camunda_url(self) -> str: 186 | return self._shared_resources.camunda_url 187 | 188 | @keyword(tags=["task"]) 189 | def get_amount_of_workloads(self, topic: str, **kwargs) -> int: 190 | """ 191 | Retrieves count of tasks. By default expects a topic name, but all parameters from the original endpoint 192 | may be provided: https://docs.camunda.org/manual/latest/reference/rest/external-task/get-query-count/ 193 | """ 194 | with self._shared_resources.api_client as api_client: 195 | api_instance = openapi_client.ExternalTaskApi(api_client) 196 | 197 | try: 198 | response: CountResultDto = api_instance.get_external_tasks_count( 199 | topic_name=topic, **kwargs 200 | ) 201 | except ApiException as e: 202 | raise ApiException( 203 | f'Failed to count workload for topic "{topic}":\n{e}' 204 | ) 205 | 206 | logger.info(f'Amount of workloads for "{topic}":\t{response.count}') 207 | return response.count 208 | 209 | @keyword(tags=["deployment"]) 210 | def deploy(self, *args): 211 | """Creates a deployment from all given files and uploads them to camunda. 212 | 213 | Return response from camunda rest api as dictionary. 214 | Further documentation: https://docs.camunda.org/manual/latest/reference/rest/deployment/post-deployment/ 215 | 216 | By default, this keyword only deploys changed models and filters duplicates. Deployment name is the filename of 217 | the first file. 218 | 219 | Example: 220 | | ${response} | *Deploy* | _../bpmn/my_model.bpnm_ | _../forms/my_forms.html_ | 221 | """ 222 | if not args: 223 | raise ValueError("Failed deploying model, because no file provided.") 224 | 225 | if len(args) > 1: 226 | # We have to use plain REST then when uploading more than 1 file. 227 | return self.deploy_multiple_files(*args) 228 | 229 | filename = os.path.basename(args[0]) 230 | 231 | with self._shared_resources.api_client as api_client: 232 | api_instance = openapi_client.DeploymentApi(api_client) 233 | data = [*args] 234 | deployment_name = filename 235 | 236 | try: 237 | response: DeploymentWithDefinitionsDto = api_instance.create_deployment( 238 | deploy_changed_only=True, 239 | enable_duplicate_filtering=True, 240 | deployment_name=deployment_name, 241 | data=data, 242 | ) 243 | logger.info(f"Response from camunda:\t{response}") 244 | except ApiException as e: 245 | raise ApiException(f"Failed to upload {filename}:\n{e}") 246 | 247 | return response.to_dict() 248 | 249 | def deploy_multiple_files(self, *args): 250 | """ 251 | # Due to https://jira.camunda.com/browse/CAM-13105 we cannot use generic camunda client when dealing with 252 | # multiple files. We have to use plain REST then. 253 | """ 254 | 255 | fields = { 256 | "deployment-name": f"{os.path.basename(args[0])}", 257 | } 258 | 259 | for file in args: 260 | filename = os.path.basename(file) 261 | fields[f"{filename}"] = ( 262 | filename, 263 | open(file, "rb"), 264 | "application/octet-stream", 265 | ) 266 | 267 | multipart_data = MultipartEncoder(fields=fields) 268 | 269 | headers = self._shared_resources.api_client.default_headers.copy() 270 | headers["Content-Type"] = multipart_data.content_type 271 | 272 | logger.debug(multipart_data.fields) 273 | 274 | response = requests.post( 275 | f"{self._shared_resources.camunda_url}/deployment/create", 276 | data=multipart_data, 277 | headers=headers, 278 | ) 279 | json = response.json() 280 | try: 281 | response.raise_for_status() 282 | logger.debug(json) 283 | except HTTPError as e: 284 | raise ApiException(json) 285 | 286 | return json 287 | 288 | @keyword(tags=["deployment"]) 289 | def get_deployments(self, deployment_id: str = None, **kwargs): 290 | """ 291 | Retrieves all deployments that match given criteria. All parameters are available from https://docs.camunda.org/manual/latest/reference/rest/deployment/get-query/ 292 | 293 | Example: 294 | | ${list_of_deployments} | get deployments | ${my_deployments_id} | 295 | | ${list_of_deployments} | get deployments | id=${my_deployments_id} | 296 | | ${list_of_deployments} | get deployments | after=2013-01-23T14:42:45.000+0200 | 297 | """ 298 | if deployment_id: 299 | kwargs["id"] = deployment_id 300 | 301 | with self._shared_resources.api_client as api_client: 302 | api_instance = openapi_client.DeploymentApi(api_client) 303 | 304 | try: 305 | response: List[DeploymentDto] = api_instance.get_deployments(**kwargs) 306 | logger.info(f"Response from camunda:\t{response}") 307 | except ApiException as e: 308 | raise ApiException(f"Failed get deployments:\n{e}") 309 | 310 | return [r.to_dict() for r in response] 311 | 312 | @keyword(tags=["message"]) 313 | def deliver_message(self, message_name, **kwargs): 314 | """ 315 | Delivers a message using Camunda REST API: https://docs.camunda.org/manual/latest/reference/rest/message/post-message/ 316 | 317 | Example: 318 | | ${result} | deliver message | msg_payment_received | 319 | | ${result} | deliver message | msg_payment_received | process_variables = ${variable_dictionary} | 320 | | ${result} | deliver message | msg_payment_received | business_key = ${correlating_business_key} | 321 | """ 322 | with self._shared_resources.api_client as api_client: 323 | correlation_message: CorrelationMessageDto = CorrelationMessageDto(**kwargs) 324 | correlation_message.message_name = message_name 325 | if not "result_enabled" in kwargs: 326 | correlation_message.result_enabled = True 327 | if "process_variables" in kwargs: 328 | correlation_message.process_variables = ( 329 | CamundaResources.dict_to_camunda_json(kwargs["process_variables"]) 330 | ) 331 | 332 | serialized_message = api_client.sanitize_for_serialization( 333 | correlation_message 334 | ) 335 | logger.debug(f"Message:\n{serialized_message}") 336 | 337 | headers = self._shared_resources.api_client.default_headers.copy() 338 | headers["Content-Type"] = "application/json" 339 | 340 | try: 341 | response = requests.post( 342 | f"{self._shared_resources.camunda_url}/message", 343 | json=serialized_message, 344 | headers=headers, 345 | ) 346 | except ApiException as e: 347 | raise ApiException(f"Failed to deliver message:\n{e}") 348 | 349 | try: 350 | response.raise_for_status() 351 | except HTTPError as e: 352 | logger.error(e) 353 | raise ApiException(response.text) 354 | 355 | if correlation_message.result_enabled: 356 | json = response.json() 357 | logger.debug(json) 358 | return json 359 | else: 360 | return {} 361 | 362 | @keyword(tags=["task"]) 363 | def fetch_workload( 364 | self, topic: str, async_response_timeout=None, use_priority=None, **kwargs 365 | ) -> Dict: 366 | """ 367 | Locks and fetches workloads from camunda on a given topic. Returns a list of variable dictionary. 368 | Each dictionary representing 1 workload from a process instance. 369 | 370 | If a process instance was fetched, the process instance is cached and can be retrieved by keyword 371 | `Get Fetch Response` 372 | 373 | The only mandatory parameter for this keyword is *topic* which is the name of the topic to fetch workload from. 374 | More parameters can be added from the Camunda documentation: https://docs.camunda.org/manual/latest/reference/rest/external-task/fetch/ 375 | 376 | If not provided, this keyword will use a lock_duration of 60000 ms (10 minutes) and set {{deserialize_value=True}} 377 | 378 | Examples: 379 | | ${input_variables} | *Create Dictionary* | _name=Robot_ | 380 | | | *start process* | _my_demo_ | _${input_variables}_ | 381 | | ${variables} | *fetch workload* | _first_task_in_demo_ | 382 | | | *Dictionary Should Contain Key* | _${variables}_ | _name_ | 383 | | | *Should Be Equal As String* | _Robot_ | _${variables}[name]_ | 384 | 385 | Example deserializing only some variables: 386 | | ${input_variables} | *Create Dictionary* | _name=Robot_ | _profession=Framework_ | 387 | | | *start process* | _my_demo_ | _${input_variables}_ | 388 | | ${variables_of_interest} | *Create List* | _profession_ | 389 | | ${variables} | *Fetch Workload* | _first_task_in_demo_ | _variables=${variables_of_interest}_ | 390 | | | *Dictionary Should Not Contain Key* | _${variables}_ | _name_ | 391 | | | *Dictionary Should Contain Key* | _${variables}_ | _profession_ | 392 | | | *Should Be Equal As String* | _Framework_ | _${variables}[profession]_ | 393 | """ 394 | api_response = [] 395 | with self._shared_resources.api_client as api_client: 396 | # Create an instance of the API class 397 | api_instance = openapi_client.ExternalTaskApi(api_client) 398 | if "lock_duration" not in kwargs: 399 | kwargs["lock_duration"] = self.DEFAULT_LOCK_DURATION 400 | if "deserialize_values" not in kwargs: 401 | kwargs["deserialize_values"] = False 402 | topic_dto = FetchExternalTaskTopicDto(topic_name=topic, **kwargs) 403 | fetch_external_tasks_dto = FetchExternalTasksDto( 404 | worker_id=self.WORKER_ID, 405 | max_tasks=1, 406 | async_response_timeout=async_response_timeout, 407 | use_priority=use_priority, 408 | topics=[topic_dto], 409 | ) 410 | 411 | try: 412 | api_response = api_instance.fetch_and_lock( 413 | fetch_external_tasks_dto=fetch_external_tasks_dto 414 | ) 415 | logger.info(api_response) 416 | except ApiException as e: 417 | raise ApiException( 418 | "Exception when calling ExternalTaskApi->fetch_and_lock: %s\n" % e 419 | ) 420 | 421 | work_items: List[LockedExternalTaskDto] = api_response 422 | if work_items: 423 | logger.debug( 424 | f"Received {len(work_items)} work_items from camunda engine for topic:\t{topic}" 425 | ) 426 | else: 427 | logger.debug( 428 | f"Received no work items from camunda engine for topic:\t{topic}" 429 | ) 430 | 431 | if not work_items: 432 | return {} 433 | 434 | self.FETCH_RESPONSE = work_items[0] 435 | 436 | variables: Dict[str, VariableValueDto] = self.FETCH_RESPONSE.variables 437 | return CamundaResources.convert_openapi_variables_to_dict(variables) 438 | 439 | @keyword(tags=["task"]) 440 | def get_fetch_response(self): 441 | """Returns cached response from the last call of `fetch workload`. 442 | 443 | The response contains all kind of data that is required for custom REST Calls. 444 | 445 | Example: 446 | | *** Settings *** | 447 | | *Library* | RequestsLibrary | 448 | | | 449 | | *** Tasks *** | 450 | | | *Create Session* | _alias=camunda_ | _url=http://localhost:8080_ | 451 | | | ${variables} | *fetch workload* | _my_first_task_in_demo_ | | 452 | | | ${fetch_response} | *get fetch response* | | | 453 | | | *POST On Session* | _camunda_ | _engine-rest/external-task/${fetch_response}[id]/complete_ | _json=${{ {'workerId': '${fetch_response}[worker_id]'} }}_ | 454 | """ 455 | if self.FETCH_RESPONSE: 456 | return self.FETCH_RESPONSE.to_dict() 457 | return self.FETCH_RESPONSE 458 | 459 | @keyword("Drop fetch response", tags=["task"]) 460 | def drop_fetch_response(self): 461 | """ 462 | Removes last process instance from cache. 463 | 464 | When you use common keywords like `complete task` or `unlock` or any other keyword finishing execution of a task, 465 | you do not need to call this keyword, as it is called implicitly. 466 | 467 | This keyword is handy, when you mix CamundaLibrary keywords and custom REST calls to Camunda API. In such 468 | scenarios you might want to empty the cache. 469 | """ 470 | self.FETCH_RESPONSE = {} 471 | 472 | @keyword(tags=["task", "complete"]) 473 | def throw_bpmn_error( 474 | self, 475 | error_code: str, 476 | error_message: str = None, 477 | variables: Dict[str, Any] = None, 478 | files: Dict = None, 479 | ): 480 | if not self.FETCH_RESPONSE: 481 | logger.warn( 482 | "No task to complete. Maybe you did not fetch and lock a workitem before?" 483 | ) 484 | else: 485 | with self._shared_resources.api_client as api_client: 486 | api_instance = openapi_client.ExternalTaskApi(api_client) 487 | variables = CamundaResources.convert_dict_to_openapi_variables( 488 | variables 489 | ) 490 | openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( 491 | files 492 | ) 493 | variables.update(openapi_files) 494 | bpmn_error = openapi_client.ExternalTaskBpmnError( 495 | worker_id=self.WORKER_ID, 496 | error_message=error_message, 497 | error_code=error_code, 498 | variables=variables, 499 | ) 500 | try: 501 | logger.debug(f"Sending BPMN error for task:\n{bpmn_error}") 502 | api_instance.handle_external_task_bpmn_error( 503 | self.FETCH_RESPONSE.id, external_task_bpmn_error=bpmn_error 504 | ) 505 | self.drop_fetch_response() 506 | except ApiException as e: 507 | raise ApiException( 508 | f"Exception when calling ExternalTaskApi->handle_external_task_bpmn_error: {e}\n" 509 | ) 510 | 511 | @keyword(tags=["task", "complete"]) 512 | def notify_failure(self, **kwargs): 513 | """ 514 | Raises a failure to Camunda. When retry counter is less than 1, an incident is created by Camunda. 515 | 516 | You can specify number of retries with the *retries* argument. If current fetched process instance already has 517 | *retries* parameter set, the *retries* argument of this keyword is ignored. Instead, the retries counter will 518 | be decreased by 1. 519 | 520 | CamundaLibrary takes care of providing the worker_id and task_id. *retry_timeout* is equal to *lock_duration* for external tasks. 521 | Check for camunda client documentation for all parameters of the request body: https://noordsestern.gitlab.io/camunda-client-for-python/7-15-0/docs/ExternalTaskApi.html#handle_failure 522 | 523 | Example: 524 | | *notify failure* | | | 525 | | *notify failure* | retries=3 | error_message=Task failed due to... | 526 | """ 527 | current_process_instance = self.FETCH_RESPONSE 528 | if not current_process_instance: 529 | logger.warn( 530 | "No task to notify failure for. Maybe you did not fetch and lock a workitem before?" 531 | ) 532 | else: 533 | with self._shared_resources.api_client as api_client: 534 | api_instance = openapi_client.ExternalTaskApi(api_client) 535 | if ( 536 | "retry_timeout" not in kwargs 537 | or None is kwargs["retry_timeout"] 538 | or not kwargs["retry_timeout"] 539 | ): 540 | kwargs["retry_timeout"] = self.DEFAULT_LOCK_DURATION 541 | 542 | if None is not current_process_instance.retries: 543 | kwargs["retries"] = current_process_instance.retries - 1 544 | 545 | external_task_failure_dto = ExternalTaskFailureDto( 546 | worker_id=self.WORKER_ID, **kwargs 547 | ) 548 | 549 | try: 550 | api_instance.handle_failure( 551 | id=current_process_instance.id, 552 | external_task_failure_dto=external_task_failure_dto, 553 | ) 554 | self.drop_fetch_response() 555 | except ApiException as e: 556 | raise ApiException( 557 | "Exception when calling ExternalTaskApi->handle_failure: %s\n" 558 | % e 559 | ) 560 | 561 | @keyword(tags=["incident"]) 562 | def get_incidents(self, **kwargs): 563 | """ 564 | Retrieves incidents matching given filter arguments. 565 | 566 | For full parameter list checkout: https://noordsestern.gitlab.io/camunda-client-for-python/7-15-0/docs/IncidentApi.html#get_incidents 567 | 568 | Example: 569 | | ${all_incidents} | *get incidents* | | 570 | | ${incidents_of_process_instance | *get incidentse* | process_instance_id=${process_instance}[process_instance_id] | 571 | """ 572 | with self._shared_resources.api_client as api_client: 573 | api_instance: IncidentApi = openapi_client.IncidentApi(api_client) 574 | 575 | try: 576 | response: List[IncidentDto] = api_instance.get_incidents(**kwargs) 577 | except ApiException as e: 578 | raise ApiException(f"Failed to get incidents:\n{e}") 579 | 580 | return [incident.to_dict() for incident in response] 581 | 582 | @keyword(tags=["task", "complete"]) 583 | def complete_task(self, result_set: Dict[str, Any] = None, files: Dict = None): 584 | """ 585 | Completes the task that was fetched before with `fetch workload`. 586 | 587 | *Requires `fetch workload` to run before this one, logs warning instead.* 588 | 589 | Additional variables can be provided as dictionary in _result_set_ . 590 | Files can be provided as dictionary of filename and patch. 591 | 592 | Examples: 593 | 594 | | _# fetch and immediately complete_ | 595 | | | *fetch workload* | _my_topic_ | 596 | | | *complete task* | | 597 | | | 598 | | _# fetch and complete with return values_ | 599 | | | *fetch workload* | _decide_on_dish_ | 600 | | ${new_variables} | *Create Dictionary* | _my_dish=salad_ | 601 | | | *complete task* | _result_set=${new_variables}_ | 602 | | | 603 | | _# fetch and complete with return values and files_ | 604 | | | *fetch workload* | _decide_on_haircut_ | 605 | | ${return_values} | *Create Dictionary* | _style=short hair_ | 606 | | ${files} | *Create Dictionary* | _should_look_like=~/favorites/beckham.jpg_ | 607 | | | *complete task* | _${return_values}_ | _${files}_ | 608 | """ 609 | if not self.FETCH_RESPONSE: 610 | logger.warn( 611 | "No task to complete. Maybe you did not fetch and lock a workitem before?" 612 | ) 613 | else: 614 | with self._shared_resources.api_client as api_client: 615 | api_instance = openapi_client.ExternalTaskApi(api_client) 616 | variables = CamundaResources.convert_dict_to_openapi_variables( 617 | result_set 618 | ) 619 | openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( 620 | files 621 | ) 622 | variables.update(openapi_files) 623 | complete_task_dto = openapi_client.CompleteExternalTaskDto( 624 | worker_id=self.WORKER_ID, variables=variables 625 | ) 626 | try: 627 | logger.debug( 628 | f"Sending to Camunda for completing Task:\n{complete_task_dto}" 629 | ) 630 | api_instance.complete_external_task_resource( 631 | self.FETCH_RESPONSE.id, 632 | complete_external_task_dto=complete_task_dto, 633 | ) 634 | self.drop_fetch_response() 635 | except ApiException as e: 636 | raise ApiException( 637 | f"Exception when calling ExternalTaskApi->complete_external_task_resource: {e}\n" 638 | ) 639 | 640 | @keyword(tags=["task", "variable", "file"]) 641 | def download_file_from_variable(self, variable_name: str) -> str: 642 | """ 643 | For performance reasons, files are not retrieved automatically during `fetch workload`. If your task requires 644 | a file that is attached to a process instance, you need to download the file explicitly. 645 | 646 | Example: 647 | | ${variables} | *fetch workload* | _first_task_in_demo_ | 648 | | | *Dictionary Should Contain Key* | _${variables}_ | _my_file_ | 649 | | ${file} | *Download File From Variable* | ${variables}[my_file] | | 650 | """ 651 | if not self.FETCH_RESPONSE: 652 | logger.warn( 653 | "Could not download file for variable. Maybe you did not fetch and lock a workitem before?" 654 | ) 655 | else: 656 | with self._shared_resources.api_client as api_client: 657 | api_instance = openapi_client.ProcessInstanceApi(api_client) 658 | 659 | try: 660 | response = api_instance.get_process_instance_variable_binary( 661 | id=self.FETCH_RESPONSE.process_instance_id, 662 | var_name=variable_name, 663 | ) 664 | logger.debug(response) 665 | except ApiException as e: 666 | raise ApiException( 667 | f"Exception when calling ExternalTaskApi->get_process_instance_variable_binary: {e}\n" 668 | ) 669 | return response 670 | 671 | @keyword(tags=["task", "complete"]) 672 | def unlock(self): 673 | """ 674 | Unlocks recent task. 675 | """ 676 | if not self.FETCH_RESPONSE: 677 | logger.warn( 678 | "No task to unlock. Maybe you did not fetch and lock a workitem before?" 679 | ) 680 | else: 681 | with self._shared_resources.api_client as api_client: 682 | api_instance = openapi_client.ExternalTaskApi(api_client) 683 | try: 684 | api_instance.unlock(self.FETCH_RESPONSE.id) 685 | self.drop_fetch_response() 686 | except ApiException as e: 687 | raise ApiException( 688 | f"Exception when calling ExternalTaskApi->unlock: {e}\n" 689 | ) 690 | 691 | @keyword(tags=["process", "deprecated"]) 692 | def start_process( 693 | self, 694 | process_key: str, 695 | variables: Dict = None, 696 | files: Dict = None, 697 | before_activity_id: str = None, 698 | after_activity_id: str = None, 699 | **kwargs, 700 | ) -> Dict: 701 | """*DEPRECATED!!* Use keyword `Start Process Instance` instead. 702 | 703 | Starts a new process instance from a process definition with given key. 704 | """ 705 | return self.start_process_instance( 706 | process_key, 707 | variables, 708 | files, 709 | before_activity_id, 710 | after_activity_id, 711 | **kwargs, 712 | ) 713 | 714 | @keyword(tags=["process"]) 715 | def start_process_instance( 716 | self, 717 | process_key: str, 718 | variables: Dict = None, 719 | files: Dict = None, 720 | before_activity_id: str = None, 721 | after_activity_id: str = None, 722 | **kwargs, 723 | ) -> Dict: 724 | """ 725 | Starts a new process instance from a process definition with given key. 726 | 727 | variables: _optional_ dictionary like: {'variable name' : 'value'} 728 | 729 | files: _optional_ dictionary like: {'variable name' : path}. will be attached to variables in Camunda 730 | 731 | before_activity_id: _optional_ id of activity at which the process starts before. *CANNOT BE USED TOGETHER WITH _after_activity_id_* 732 | 733 | after_activity_id: _optional_ id of activity at which the process starts after. *CANNOT BE USED TOGETHER WITH _before_activity_id_* 734 | 735 | Returns response from Camunda as dictionary 736 | 737 | == Examples == 738 | | `start process` | apply for job promotion | _variables_= { 'employee' : 'John Doe', 'permission_for_application_granted' : True} | _files_ = { 'cv' : 'documents/my_life.md'} | _after_activity_id_ = 'Activity_ask_boss_for_persmission' | 739 | | `start process` | apply for promotion | business_key=John again | 740 | """ 741 | if not process_key: 742 | raise ValueError("Error starting process. No process key provided.") 743 | 744 | if before_activity_id and after_activity_id: 745 | raise AssertionError( 746 | "2 activity ids provided. Cannot start before and after an activity." 747 | ) 748 | 749 | with self._shared_resources.api_client as api_client: 750 | api_instance: ProcessDefinitionApi = openapi_client.ProcessDefinitionApi( 751 | api_client 752 | ) 753 | openapi_variables = CamundaResources.convert_dict_to_openapi_variables( 754 | variables 755 | ) 756 | openapi_files = CamundaResources.convert_file_dict_to_openapi_variables( 757 | files 758 | ) 759 | openapi_variables.update(openapi_files) 760 | 761 | if before_activity_id or after_activity_id: 762 | instruction: ProcessInstanceModificationInstructionDto = ( 763 | ProcessInstanceModificationInstructionDto( 764 | type=( 765 | "startBeforeActivity" 766 | if before_activity_id 767 | else "startAfterActivity" 768 | ), 769 | activity_id=( 770 | before_activity_id 771 | if before_activity_id 772 | else after_activity_id 773 | ), 774 | ) 775 | ) 776 | kwargs.update(start_instructions=[instruction]) 777 | 778 | start_process_instance_dto: StartProcessInstanceDto = ( 779 | StartProcessInstanceDto(variables=openapi_variables, **kwargs) 780 | ) 781 | 782 | try: 783 | response: ProcessInstanceWithVariablesDto = ( 784 | api_instance.start_process_instance_by_key( 785 | key=process_key, 786 | start_process_instance_dto=start_process_instance_dto, 787 | ) 788 | ) 789 | except ApiException as e: 790 | raise ApiException(f"Failed to start process {process_key}:\n{e}") 791 | logger.info(f"Response:\n{response}") 792 | 793 | return response.to_dict() 794 | 795 | @keyword(tags=["process"]) 796 | def delete_process_instance(self, process_instance_id): 797 | """ 798 | USE WITH CARE: Deletes a process instance by id. All data in this process instance will be lost. 799 | """ 800 | with self._shared_resources.api_client as api_client: 801 | api_instance = openapi_client.ProcessInstanceApi(api_client) 802 | 803 | try: 804 | response = api_instance.delete_process_instance(id=process_instance_id) 805 | except ApiException as e: 806 | raise ApiException( 807 | f"Failed to delete process instance {process_instance_id}:\n{e}" 808 | ) 809 | 810 | @keyword(tags=["process"]) 811 | def get_all_active_process_instances(self, process_definition_key): 812 | """ 813 | Returns a list of process instances that are active for a certain process definition identified by key. 814 | """ 815 | return self.get_process_instances( 816 | process_definition_key=process_definition_key, active="true" 817 | ) 818 | 819 | @keyword(tags=["process"]) 820 | def get_process_instances(self, **kwargs): 821 | """ 822 | Queries Camunda for process instances that match certain criteria. 823 | 824 | *Be aware, that boolean value must be strings either 'true' or 'false'* 825 | 826 | == Arguments == 827 | | async_req | execute request asynchronously | 828 | | sort_by | Sort the results lexicographically by a given criterion. Must be used in conjunction with the sortOrder parameter. | 829 | | sort_order | Sort the results in a given order. Values may be asc for ascending order or desc for descending order. Must be used in conjunction with the sortBy parameter. | 830 | | first_result | Pagination of results. Specifies the index of the first result to return. | 831 | | max_results | Pagination of results. Specifies the maximum number of results to return. Will return less results if there are no more results left. | 832 | | process_instance_ids | Filter by a comma-separated list of process instance ids. | 833 | | business_key | Filter by process instance business key. | 834 | | business_key_like | Filter by process instance business key that the parameter is a substring of. | 835 | | case_instance_id | Filter by case instance id. | 836 | | process_definition_id | Filter by the deployment the id belongs to. | 837 | | process_definition_key | Filter by the key of the process definition the instances run on. | 838 | | process_definition_key_in | Filter by a comma-separated list of process definition keys. A process instance must have one of the given process definition keys. | 839 | | process_definition_key_not_in | Exclude instances by a comma-separated list of process definition keys. A process instance must not have one of the given process definition keys. | 840 | | deployment_id | Filter by the deployment the id belongs to. | 841 | | super_process_instance | Restrict query to all process instances that are sub process instances of the given process instance. Takes a process instance id. | 842 | | sub_process_instance | Restrict query to all process instances that have the given process instance as a sub process instance. Takes a process instance id. | 843 | | super_case_instance | Restrict query to all process instances that are sub process instances of the given case instance. Takes a case instance id. | 844 | | sub_case_instance | Restrict query to all process instances that have the given case instance as a sub case instance. Takes a case instance id. | 845 | | active | Only include active process instances. Value may only be true, as false is the default behavior. | 846 | | suspended | Only include suspended process instances. Value may only be true, as false is the default behavior. | 847 | | with_incident | Filter by presence of incidents. Selects only process instances that have an incident. | 848 | | incident_id | Filter by the incident id. | 849 | | incident_type | Filter by the incident type. See the [User Guide](https://docs.camunda.org/manual/latest/user-guide/process-engine/incidents/#incident-types) for a list of incident types. | 850 | | incident_message | Filter by the incident message. Exact match. | 851 | | incident_message_like | Filter by the incident message that the parameter is a substring of. | 852 | | tenant_id_in | Filter by a comma-separated list of tenant ids. A process instance must have one of the given tenant ids. | 853 | | without_tenant_id | Only include process instances which belong to no tenant. | 854 | | process_definition_without_tenant_id | Only include process instances which process definition has no tenant id. | 855 | | activity_id_in | Filter by a comma-separated list of activity ids. A process instance must currently wait in a leaf activity with one of the given activity ids. | 856 | | root_process_instances | Restrict the query to all process instances that are top level process instances. | 857 | | leaf_process_instances | Restrict the query to all process instances that are leaf instances. (i.e. don't have any sub instances). | 858 | | variables | Only include process instances that have variables with certain values. Variable filtering expressions are comma-separated and are structured as follows: A valid parameter value has the form `key_operator_value`. `key` is the variable name, `operator` is the comparison operator to be used and `value` the variable value. **Note**: Values are always treated as String objects on server side. Valid `operator` values are: `eq` - equal to; `neq` - not equal to; `gt` - greater than; `gteq` - greater than or equal to; `lt` - lower than; `lteq` - lower than or equal to; `like`. `key` and `value` may not contain underscore or comma characters. | 859 | | variable_names_ignore_case | Match all variable names in this query case-insensitively. If set to true variableName and variablename are treated as equal. | 860 | | variable_values_ignore_case | Match all variable values in this query case-insensitively. If set to true variableValue and variablevalue are treated as equal. | 861 | | _preload_content | if False, the urllib3.HTTPResponse object will 862 | be returned without reading/decoding response 863 | data. Default is True. | 864 | | _request_timeout | timeout setting for this request. If one 865 | number provided, it will be total request 866 | timeout. It can also be a pair (tuple) of 867 | (connection, read) timeouts. | 868 | """ 869 | with self._shared_resources.api_client as api_client: 870 | api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( 871 | api_client 872 | ) 873 | 874 | try: 875 | response: List[ProcessInstanceDto] = api_instance.get_process_instances( 876 | **kwargs 877 | ) 878 | except ApiException as e: 879 | raise ApiException(f"Failed to get process instances of process:\n{e}") 880 | 881 | return [process_instance.to_dict() for process_instance in response] 882 | 883 | @keyword(tags=["version"]) 884 | def get_version(self): 885 | """ 886 | Returns Version of Camunda. 887 | 888 | == Example == 889 | | ${camunda_version_dto} | Get Version | 890 | | ${camunda_version} | Set Variable | ${camunda_version_dto.version} | 891 | """ 892 | with self._shared_resources.api_client as api_client: 893 | api_instance: VersionApi = openapi_client.VersionApi(api_client) 894 | return api_instance.get_rest_api_version() 895 | 896 | @keyword(tags=["process"]) 897 | def get_process_definitions(self, **kwargs): 898 | """ 899 | Returns a list of process definitions that fulfill given parameters. 900 | 901 | See Rest API documentation on ``https://docs.camunda.org/manual`` for available parameters. 902 | 903 | == Example == 904 | | ${list} | Get Process Definitions | name=my_process_definition | 905 | """ 906 | with self._shared_resources.api_client as api_client: 907 | api_instance: ProcessDefinitionApi = openapi_client.ProcessDefinitionApi( 908 | api_client 909 | ) 910 | 911 | try: 912 | response = api_instance.get_process_definitions(**kwargs) 913 | except ApiException as e: 914 | raise ApiException(f"Failed to get process definitions:\n{e}") 915 | 916 | return response 917 | 918 | @keyword(tags=["process"]) 919 | def get_activity_instance(self, id: str): 920 | """ 921 | Returns an Activity Instance (Tree) for a given process instance. 922 | 923 | == Example == 924 | | ${tree} | Get Activity Instance | id=fcab43bc-b970-11eb-be75-0242ac110002 | 925 | 926 | https://docs.camunda.org/manual/7.5/reference/rest/process-instance/get-activity-instances/ 927 | """ 928 | with self._shared_resources.api_client as api_client: 929 | api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( 930 | api_client 931 | ) 932 | 933 | try: 934 | response: ActivityInstanceDto = api_instance.get_activity_instance_tree( 935 | id 936 | ) 937 | except ApiException as e: 938 | raise ApiException( 939 | f"failed to get activity tree for process instance with id {id}:\n{e}" 940 | ) 941 | return response.to_dict() 942 | 943 | @keyword(tags=["process"]) 944 | def get_process_instance_variable( 945 | self, 946 | process_instance_id: str, 947 | variable_name: str, 948 | auto_type_conversion: bool = True, 949 | ): 950 | """ 951 | Returns the variable with the given name from the process instance with 952 | the given process_instance_id. 953 | 954 | Parameters: 955 | - ``process_instance_id``: ID of the target process instance 956 | - ``variable_name``: name of the variable to read 957 | - ``auto_type_conversion``: Converts JSON structures automatically in to python data structures. Default: True. When False, values are not retrieved for JSON variables, but metadata is. Only useful when you want to verify that Camunda holds certain data types.% 958 | 959 | == Example == 960 | | ${variable} | Get Process Instance Variable | 961 | | ... | process_instance_id=fcab43bc-b970-11eb-be75-0242ac110002 | 962 | | ... | variable_name=foo | 963 | 964 | See also: 965 | https://docs.camunda.org/manual/latest/reference/rest/process-instance/variables/get-single-variable/ 966 | """ 967 | with self._shared_resources.api_client as api_client: 968 | api_instance: ProcessInstanceApi = openapi_client.ProcessInstanceApi( 969 | api_client 970 | ) 971 | 972 | try: 973 | response = api_instance.get_process_instance_variable( 974 | id=process_instance_id, 975 | var_name=variable_name, 976 | deserialize_value=not auto_type_conversion, 977 | ) 978 | except ApiException as e: 979 | raise ApiException( 980 | f"Failed to get variable {variable_name} from " 981 | f"process instance {process_instance_id}:\n{e}" 982 | ) 983 | if auto_type_conversion: 984 | return CamundaResources.convert_variable_dto(response) 985 | return response 986 | 987 | @keyword(tags=["decision"]) 988 | def evaluate_decision(self, key: str, variables: dict) -> list: 989 | """ 990 | Evaluates a given decision and returns the result. 991 | The input values of the decision have to be supplied with `variables`. 992 | 993 | == Example == 994 | | ${variables} | Create Dictionary | my_input=42 | 995 | | ${response} | Evaluate Decision | my_decision_table | ${variables} | 996 | """ 997 | with self._shared_resources.api_client as api_client: 998 | api_instance = openapi_client.DecisionDefinitionApi(api_client) 999 | dto = CamundaResources.convert_dict_to_openapi_variables(variables) 1000 | try: 1001 | response = api_instance.evaluate_decision_by_key( 1002 | key=key, 1003 | evaluate_decision_dto=openapi_client.EvaluateDecisionDto(dto), 1004 | ) 1005 | return [ 1006 | CamundaResources.convert_openapi_variables_to_dict(r) 1007 | for r in response 1008 | ] 1009 | except ApiException as e: 1010 | raise ApiException(f"Failed to evaluate decision {key}:\n{e}") 1011 | --------------------------------------------------------------------------------