--------------------------------------------------------------------------------
/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 | [](https://pypi.python.org/pypi/robotframework-camunda/) [](https://gitlab.com/robotframework-camunda-demos/robotframework-camunda-mirror/-/commits/master) [](https://pypi.com/project/robotframework-camunda/) [](https://pypi.org/project/robotframework-camunda) [](https://pypi.python.org/pypi/robotframework-camunda/) [](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 |
--------------------------------------------------------------------------------