operaton-external-task-client-python3 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/__init__.py +0 -0
- examples/bpmn_error_example.py +75 -0
- examples/correlate_message.py +11 -0
- examples/event_subprocess_example.py +50 -0
- examples/examples_auth_basic/__init__.py +0 -0
- examples/examples_auth_basic/fetch_and_execute.py +31 -0
- examples/examples_auth_basic/get_process_instance.py +12 -0
- examples/examples_auth_basic/start_process.py +15 -0
- examples/examples_auth_basic/task_handler_example.py +44 -0
- examples/fetch_and_execute.py +30 -0
- examples/get_process_instance.py +12 -0
- examples/retry_task_example.py +58 -0
- examples/start_process.py +14 -0
- examples/task_handler_example.py +44 -0
- examples/tasks_example.py +36 -0
- operaton/__init__.py +0 -0
- operaton/client/__init__.py +0 -0
- operaton/client/async_external_task_client.py +171 -0
- operaton/client/engine_client.py +180 -0
- operaton/client/external_task_client.py +166 -0
- operaton/client/tests/__init__.py +0 -0
- operaton/client/tests/test_async_external_task_client.py +128 -0
- operaton/client/tests/test_async_external_task_client_auth.py +42 -0
- operaton/client/tests/test_async_external_task_client_bearer.py +43 -0
- operaton/client/tests/test_engine_client.py +228 -0
- operaton/client/tests/test_engine_client_auth.py +231 -0
- operaton/client/tests/test_engine_client_bearer.py +237 -0
- operaton/client/tests/test_external_task_client.py +17 -0
- operaton/client/tests/test_external_task_client_auth.py +19 -0
- operaton/client/tests/test_external_task_client_bearer.py +24 -0
- operaton/external_task/__init__.py +0 -0
- operaton/external_task/async_external_task_executor.py +91 -0
- operaton/external_task/async_external_task_worker.py +181 -0
- operaton/external_task/external_task.py +173 -0
- operaton/external_task/external_task_executor.py +88 -0
- operaton/external_task/external_task_worker.py +92 -0
- operaton/external_task/tests/__init__.py +0 -0
- operaton/external_task/tests/test_async_external_task_executor.py +139 -0
- operaton/external_task/tests/test_async_external_task_worker.py +129 -0
- operaton/external_task/tests/test_external_task.py +106 -0
- operaton/external_task/tests/test_external_task_executor.py +200 -0
- operaton/external_task/tests/test_external_task_worker.py +147 -0
- operaton/process_definition/__init__.py +0 -0
- operaton/process_definition/process_definition_client.py +123 -0
- operaton/process_definition/tests/__init__.py +0 -0
- operaton/process_definition/tests/test_process_definition_client.py +181 -0
- operaton/utils/__init__.py +0 -0
- operaton/utils/auth_basic.py +28 -0
- operaton/utils/auth_bearer.py +28 -0
- operaton/utils/log_utils.py +31 -0
- operaton/utils/response_utils.py +35 -0
- operaton/utils/tests/test_auth_basic.py +30 -0
- operaton/utils/tests/test_auth_bearer.py +27 -0
- operaton/utils/tests/test_response_utils.py +43 -0
- operaton/utils/tests/test_utils.py +21 -0
- operaton/utils/utils.py +14 -0
- operaton/variables/__init__.py +0 -0
- operaton/variables/properties.py +27 -0
- operaton/variables/tests/test_properties.py +20 -0
- operaton/variables/tests/test_variables.py +60 -0
- operaton/variables/variables.py +45 -0
- operaton_external_task_client_python3-1.0.0.dist-info/METADATA +258 -0
- operaton_external_task_client_python3-1.0.0.dist-info/RECORD +66 -0
- operaton_external_task_client_python3-1.0.0.dist-info/WHEEL +5 -0
- operaton_external_task_client_python3-1.0.0.dist-info/licenses/LICENSE +201 -0
- operaton_external_task_client_python3-1.0.0.dist-info/top_level.txt +2 -0
examples/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
3
|
+
|
|
4
|
+
from operaton.external_task.external_task import ExternalTask
|
|
5
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
6
|
+
from operaton.utils.log_utils import log_with_context
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
default_config = {
|
|
11
|
+
"maxTasks": 1,
|
|
12
|
+
"lockDuration": 10000,
|
|
13
|
+
"asyncResponseTimeout": 30000,
|
|
14
|
+
"retries": 3,
|
|
15
|
+
"retryTimeout": 5000,
|
|
16
|
+
"sleepSeconds": 30,
|
|
17
|
+
"isDebug": True,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_image(task: ExternalTask):
|
|
22
|
+
"""
|
|
23
|
+
To simulate BPMN/Failure/Success, this handler uses image name variable (to be passed when launching the process)
|
|
24
|
+
"""
|
|
25
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
26
|
+
"TASK_ID": task.get_task_id(),
|
|
27
|
+
"TOPIC": task.get_topic_name()}
|
|
28
|
+
|
|
29
|
+
log_with_context("executing validate_image", log_context)
|
|
30
|
+
img_name = task.get_variable('imgName')
|
|
31
|
+
|
|
32
|
+
if "poor" in img_name:
|
|
33
|
+
return task.bpmn_error("POOR_QUALITY_IMAGE", "Image quality is bad",
|
|
34
|
+
{"img_rejection_code": "POOR_QUALITY_CODE_XX",
|
|
35
|
+
"img_rejection_reason": f"Image quality must be at least GOOD"})
|
|
36
|
+
elif "jpg" in img_name:
|
|
37
|
+
return task.complete({"img_approved": True})
|
|
38
|
+
elif "corrupt" in img_name:
|
|
39
|
+
return task.failure("Cannot validate image", "image is corrupted", 0, default_config.get("retryTimeout"))
|
|
40
|
+
else:
|
|
41
|
+
return task.bpmn_error("INVALID_IMAGE", "Image extension must be jpg",
|
|
42
|
+
{"img_rejection_code": "INVALID_IMG_NAME",
|
|
43
|
+
"img_rejection_reason": f"Image name {img_name} is invalid"})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generic_task_handler(task: ExternalTask):
|
|
47
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
48
|
+
"TASK_ID": task.get_task_id(),
|
|
49
|
+
"TOPIC": task.get_topic_name()}
|
|
50
|
+
|
|
51
|
+
log_with_context("executing generic task handler", log_context)
|
|
52
|
+
return task.complete()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
configure_logging()
|
|
57
|
+
topics = [("VALIDATE_IMAGE", validate_image),
|
|
58
|
+
# ("APPROVE_IMAGE", generic_task_handler),
|
|
59
|
+
# ("REJECT_IMAGE", generic_task_handler),
|
|
60
|
+
# ("ENHANCE_IMAGE_QUALITY", generic_task_handler),
|
|
61
|
+
]
|
|
62
|
+
executor = ThreadPoolExecutor(max_workers=len(topics))
|
|
63
|
+
for index, topic_handler in enumerate(topics):
|
|
64
|
+
topic = topic_handler[0]
|
|
65
|
+
handler_func = topic_handler[1]
|
|
66
|
+
executor.submit(ExternalTaskWorker(worker_id=index, config=default_config).subscribe, topic, handler_func)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def configure_logging():
|
|
70
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
71
|
+
handlers=[logging.StreamHandler()])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from operaton.client.engine_client import EngineClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
client = EngineClient()
|
|
6
|
+
resp_json = client.correlate_message("CANCEL_MESSAGE", business_key="b4a6f392-12ab-11eb-80ef-acde48001122")
|
|
7
|
+
print(resp_json)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if __name__ == '__main__':
|
|
11
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
3
|
+
|
|
4
|
+
from operaton.external_task.external_task import ExternalTask
|
|
5
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
6
|
+
from operaton.utils.log_utils import log_with_context
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
default_config = {
|
|
11
|
+
"maxTasks": 1,
|
|
12
|
+
"lockDuration": 10000,
|
|
13
|
+
"asyncResponseTimeout": 30000,
|
|
14
|
+
"retries": 3,
|
|
15
|
+
"retryTimeout": 5000,
|
|
16
|
+
"sleepSeconds": 30,
|
|
17
|
+
"isDebug": True,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generic_task_handler(task: ExternalTask):
|
|
22
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
23
|
+
"TASK_ID": task.get_task_id(),
|
|
24
|
+
"TOPIC": task.get_topic_name()}
|
|
25
|
+
|
|
26
|
+
log_with_context("executing generic task handler", log_context)
|
|
27
|
+
return task.complete()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
configure_logging()
|
|
32
|
+
topics = [
|
|
33
|
+
("STEP_1", generic_task_handler),
|
|
34
|
+
# ("STEP_2", generic_task_handler),
|
|
35
|
+
# ("CLEAN_UP", generic_task_handler),
|
|
36
|
+
]
|
|
37
|
+
executor = ThreadPoolExecutor(max_workers=len(topics))
|
|
38
|
+
for index, topic_handler in enumerate(topics):
|
|
39
|
+
topic = topic_handler[0]
|
|
40
|
+
handler_func = topic_handler[1]
|
|
41
|
+
executor.submit(ExternalTaskWorker(worker_id=index, config=default_config).subscribe, topic, handler_func)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def configure_logging():
|
|
45
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
46
|
+
handlers=[logging.StreamHandler()])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == '__main__':
|
|
50
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
4
|
+
from task_handler_example import handle_task
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
default_config = {
|
|
9
|
+
"auth_basic": {"username": "demo", "password": "demo"},
|
|
10
|
+
"maxTasks": 1,
|
|
11
|
+
"lockDuration": 10000,
|
|
12
|
+
"asyncResponseTimeout": 0,
|
|
13
|
+
"isDebug": True,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
configure_logging()
|
|
19
|
+
topics = ["PARALLEL_STEP_1", "PARALLEL_STEP_2", "COMBINE_STEP"]
|
|
20
|
+
for index, topic in enumerate(topics):
|
|
21
|
+
ExternalTaskWorker(worker_id=index, config=default_config) \
|
|
22
|
+
.fetch_and_execute(topic_names=topic, action=handle_task, process_variables={"strVar": "hello"})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def configure_logging():
|
|
26
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
27
|
+
handlers=[logging.StreamHandler()])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == '__main__':
|
|
31
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from operaton.client.engine_client import EngineClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
client = EngineClient(config={"auth_basic": {"username": "demo", "password": "demo"}})
|
|
6
|
+
resp_json = client.get_process_instance("PARALLEL_STEPS_EXAMPLE", ["intVar_eq_1", "strVar_eq_hello"],
|
|
7
|
+
["6172cdf0-7b32-4460-9da0-ded5107aa977"])
|
|
8
|
+
print(resp_json)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__':
|
|
12
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from operaton.client.engine_client import EngineClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
client = EngineClient(config={"auth_basic": {"username": "demo", "password": "demo"}})
|
|
8
|
+
resp_json = client.start_process(
|
|
9
|
+
process_key="PARALLEL_STEPS_EXAMPLE", variables={"intVar": "1", "strVar": "hello"},
|
|
10
|
+
tenant_id="6172cdf0-7b32-4460-9da0-ded5107aa977", business_key=str(uuid.uuid1()))
|
|
11
|
+
print(resp_json)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == '__main__':
|
|
15
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from random import randint
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from operaton.external_task.external_task import ExternalTask
|
|
7
|
+
from operaton.utils.log_utils import log_with_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_task(task: ExternalTask):
|
|
11
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
12
|
+
"TASK_ID": task.get_task_id(),
|
|
13
|
+
"TOPIC": task.get_topic_name()}
|
|
14
|
+
|
|
15
|
+
log_with_context(f"handle_task started: business key = {task.get_business_key()}", log_context)
|
|
16
|
+
|
|
17
|
+
# simulate task execution
|
|
18
|
+
execution_time = randint(0, 10)
|
|
19
|
+
log_with_context(f"handle_task - business logic execution started for task: "
|
|
20
|
+
f"it will execute for {execution_time} seconds", log_context)
|
|
21
|
+
time.sleep(execution_time)
|
|
22
|
+
|
|
23
|
+
# simulate that task results randomly into failure/BPMN error/complete
|
|
24
|
+
failure = random_true()
|
|
25
|
+
bpmn_error = False if failure else random_true()
|
|
26
|
+
# override the values to simulate success/failure/BPMN error explicitly (if needed)
|
|
27
|
+
failure, bpmn_error = False, False
|
|
28
|
+
log_with_context(f"handle_task - business logic executed: failure: {failure}, bpmn_error: {bpmn_error}",
|
|
29
|
+
log_context)
|
|
30
|
+
|
|
31
|
+
return __handle_task_result(task, failure, bpmn_error)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __handle_task_result(task, failure, bpmn_error):
|
|
35
|
+
if failure:
|
|
36
|
+
return task.failure("task failed", "failed task details", 3, 5000)
|
|
37
|
+
elif bpmn_error:
|
|
38
|
+
return task.bpmn_error("BPMN_ERROR_CODE")
|
|
39
|
+
return task.complete({"success": True, "task_completed_on": str(datetime.now())})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def random_true():
|
|
43
|
+
current_milli_time = int(round(time.time() * 1000))
|
|
44
|
+
return current_milli_time % 2 == 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
4
|
+
from examples.task_handler_example import handle_task
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
default_config = {
|
|
9
|
+
"maxTasks": 1,
|
|
10
|
+
"lockDuration": 10000,
|
|
11
|
+
"asyncResponseTimeout": 0,
|
|
12
|
+
"isDebug": True,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
configure_logging()
|
|
18
|
+
topics = ["PARALLEL_STEP_1", "PARALLEL_STEP_2", "COMBINE_STEP"]
|
|
19
|
+
for index, topic in enumerate(topics):
|
|
20
|
+
ExternalTaskWorker(worker_id=index, config=default_config) \
|
|
21
|
+
.fetch_and_execute(topic_names=topic, action=handle_task, process_variables={"strVar": "hello"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def configure_logging():
|
|
25
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
26
|
+
handlers=[logging.StreamHandler()])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == '__main__':
|
|
30
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from operaton.client.engine_client import EngineClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
client = EngineClient()
|
|
6
|
+
resp_json = client.get_process_instance("PARALLEL_STEPS_EXAMPLE", ["intVar_eq_1", "strVar_eq_hello"],
|
|
7
|
+
["6172cdf0-7b32-4460-9da0-ded5107aa977"])
|
|
8
|
+
print(resp_json)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__':
|
|
12
|
+
main()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
3
|
+
|
|
4
|
+
from operaton.external_task.external_task import ExternalTask
|
|
5
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
6
|
+
from operaton.utils.log_utils import log_with_context
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
default_config = {
|
|
11
|
+
"maxTasks": 1,
|
|
12
|
+
"lockDuration": 10000,
|
|
13
|
+
"asyncResponseTimeout": 30000,
|
|
14
|
+
"retries": 3,
|
|
15
|
+
"retryTimeout": 5000,
|
|
16
|
+
"sleepSeconds": 30,
|
|
17
|
+
"isDebug": True,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generic_task_handler(task: ExternalTask):
|
|
22
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
23
|
+
"TASK_ID": task.get_task_id(),
|
|
24
|
+
"TOPIC": task.get_topic_name()}
|
|
25
|
+
|
|
26
|
+
log_with_context("executing generic task handler", log_context)
|
|
27
|
+
return task.complete()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def fail_task_handler(task: ExternalTask):
|
|
31
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
32
|
+
"TASK_ID": task.get_task_id(),
|
|
33
|
+
"TOPIC": task.get_topic_name()}
|
|
34
|
+
|
|
35
|
+
log_with_context("executing fail_task_handler", log_context)
|
|
36
|
+
return task.failure("task failed", "task failed forced", 0, 10)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
configure_logging()
|
|
41
|
+
topics = [
|
|
42
|
+
("TASK_1", fail_task_handler),
|
|
43
|
+
("TASK_2", fail_task_handler),
|
|
44
|
+
]
|
|
45
|
+
executor = ThreadPoolExecutor(max_workers=len(topics))
|
|
46
|
+
for index, topic_handler in enumerate(topics):
|
|
47
|
+
topic = topic_handler[0]
|
|
48
|
+
handler_func = topic_handler[1]
|
|
49
|
+
executor.submit(ExternalTaskWorker(worker_id=index, config=default_config).subscribe, topic, handler_func)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def configure_logging():
|
|
53
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
54
|
+
handlers=[logging.StreamHandler()])
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from operaton.client.engine_client import EngineClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
client = EngineClient()
|
|
8
|
+
resp_json = client.start_process(process_key="PARALLEL_STEPS_EXAMPLE", variables={"intVar": "1", "strVar": "hello"},
|
|
9
|
+
tenant_id="6172cdf0-7b32-4460-9da0-ded5107aa977", business_key=str(uuid.uuid1()))
|
|
10
|
+
print(resp_json)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == '__main__':
|
|
14
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from random import randint
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from operaton.external_task.external_task import ExternalTask
|
|
7
|
+
from operaton.utils.log_utils import log_with_context
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_task(task: ExternalTask):
|
|
11
|
+
log_context = {"WORKER_ID": task.get_worker_id(),
|
|
12
|
+
"TASK_ID": task.get_task_id(),
|
|
13
|
+
"TOPIC": task.get_topic_name()}
|
|
14
|
+
|
|
15
|
+
log_with_context(f"handle_task started: business key = {task.get_business_key()}", log_context)
|
|
16
|
+
|
|
17
|
+
# simulate task execution
|
|
18
|
+
execution_time = randint(0, 10)
|
|
19
|
+
log_with_context(f"handle_task - business logic execution started for task: "
|
|
20
|
+
f"it will execute for {execution_time} seconds", log_context)
|
|
21
|
+
time.sleep(execution_time)
|
|
22
|
+
|
|
23
|
+
# simulate that task results randomly into failure/BPMN error/complete
|
|
24
|
+
failure = random_true()
|
|
25
|
+
bpmn_error = False if failure else random_true()
|
|
26
|
+
# override the values to simulate success/failure/BPMN error explicitly (if needed)
|
|
27
|
+
failure, bpmn_error = False, False
|
|
28
|
+
log_with_context(f"handle_task - business logic executed: failure: {failure}, bpmn_error: {bpmn_error}",
|
|
29
|
+
log_context)
|
|
30
|
+
|
|
31
|
+
return __handle_task_result(task, failure, bpmn_error)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __handle_task_result(task, failure, bpmn_error):
|
|
35
|
+
if failure:
|
|
36
|
+
return task.failure("task failed", "failed task details", 3, 5000)
|
|
37
|
+
elif bpmn_error:
|
|
38
|
+
return task.bpmn_error("BPMN_ERROR_CODE")
|
|
39
|
+
return task.complete({"success": True, "task_completed_on": str(datetime.now())})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def random_true():
|
|
43
|
+
current_milli_time = int(round(time.time() * 1000))
|
|
44
|
+
return current_milli_time % 2 == 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from concurrent.futures.thread import ThreadPoolExecutor
|
|
3
|
+
|
|
4
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
5
|
+
from examples.task_handler_example import handle_task
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
default_config = {
|
|
10
|
+
"maxTasks": 1,
|
|
11
|
+
"lockDuration": 10000,
|
|
12
|
+
"asyncResponseTimeout": 3000,
|
|
13
|
+
"retries": 3,
|
|
14
|
+
"retryTimeout": 5000,
|
|
15
|
+
"sleepSeconds": 30,
|
|
16
|
+
"isDebug": True,
|
|
17
|
+
"httpTimeoutMillis": 3000,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
configure_logging()
|
|
23
|
+
topics = ["PARALLEL_STEP_1", "PARALLEL_STEP_2", "COMBINE_STEP"]
|
|
24
|
+
executor = ThreadPoolExecutor(max_workers=len(topics))
|
|
25
|
+
for index, topic in enumerate(topics):
|
|
26
|
+
executor.submit(ExternalTaskWorker(worker_id=index, config=default_config).subscribe, topic, handle_task,
|
|
27
|
+
{"strVar": "hello"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def configure_logging():
|
|
31
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s",
|
|
32
|
+
handlers=[logging.StreamHandler()])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == '__main__':
|
|
36
|
+
main()
|
operaton/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from operaton.client.engine_client import ENGINE_LOCAL_BASE_URL
|
|
7
|
+
from operaton.utils.log_utils import log_with_context
|
|
8
|
+
from operaton.utils.response_utils import raise_exception_if_not_ok
|
|
9
|
+
from operaton.utils.utils import str_to_list
|
|
10
|
+
from operaton.utils.auth_basic import AuthBasic, obfuscate_password
|
|
11
|
+
from operaton.utils.auth_bearer import AuthBearer
|
|
12
|
+
from operaton.variables.variables import Variables
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncExternalTaskClient:
|
|
18
|
+
default_config = {
|
|
19
|
+
"maxConcurrentTasks": 10, # Number of concurrent tasks you can process
|
|
20
|
+
"lockDuration": 300000, # in milliseconds
|
|
21
|
+
"asyncResponseTimeout": 30000,
|
|
22
|
+
"retries": 3,
|
|
23
|
+
"retryTimeout": 300000,
|
|
24
|
+
"httpTimeoutMillis": 30000,
|
|
25
|
+
"timeoutDeltaMillis": 5000,
|
|
26
|
+
"includeExtensionProperties": True, # enables Camunda Extension Properties
|
|
27
|
+
"deserializeValues": True, # deserialize values when fetch a task by default
|
|
28
|
+
"usePriority": False,
|
|
29
|
+
"sorting": None
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def __init__(self, worker_id, engine_base_url=ENGINE_LOCAL_BASE_URL, config=None):
|
|
33
|
+
config = config if config is not None else {}
|
|
34
|
+
self.worker_id = worker_id
|
|
35
|
+
self.external_task_base_url = engine_base_url + "/external-task"
|
|
36
|
+
self.config = type(self).default_config.copy()
|
|
37
|
+
self.config.update(config)
|
|
38
|
+
self.is_debug = config.get('isDebug', False)
|
|
39
|
+
self.http_timeout_seconds = self.config.get('httpTimeoutMillis') / 1000
|
|
40
|
+
self._log_with_context(f"Created External Task client with config: {obfuscate_password(self.config)}")
|
|
41
|
+
|
|
42
|
+
def get_fetch_and_lock_url(self):
|
|
43
|
+
return f"{self.external_task_base_url}/fetchAndLock"
|
|
44
|
+
|
|
45
|
+
async def fetch_and_lock(self, topic_names, process_variables=None, variables=None):
|
|
46
|
+
url = self.get_fetch_and_lock_url()
|
|
47
|
+
body = {
|
|
48
|
+
"workerId": str(self.worker_id), # convert to string to make it JSON serializable
|
|
49
|
+
"maxTasks": 1,
|
|
50
|
+
"topics": self._get_topics(topic_names, process_variables, variables),
|
|
51
|
+
"asyncResponseTimeout": self.config["asyncResponseTimeout"],
|
|
52
|
+
"usePriority": self.config["usePriority"],
|
|
53
|
+
"sorting": self.config["sorting"]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if self.is_debug:
|
|
57
|
+
self._log_with_context(f"Trying to fetch and lock with request payload: {body}")
|
|
58
|
+
http_timeout_seconds = self.__get_fetch_and_lock_http_timeout_seconds()
|
|
59
|
+
|
|
60
|
+
async with httpx.AsyncClient() as client:
|
|
61
|
+
response = await client.post(url, headers=self._get_headers(), json=body, timeout=http_timeout_seconds)
|
|
62
|
+
raise_exception_if_not_ok(response)
|
|
63
|
+
|
|
64
|
+
resp_json = response.json()
|
|
65
|
+
if self.is_debug:
|
|
66
|
+
self._log_with_context(f"Fetch and lock response JSON: {resp_json} for request: {body}")
|
|
67
|
+
return resp_json
|
|
68
|
+
|
|
69
|
+
def __get_fetch_and_lock_http_timeout_seconds(self):
|
|
70
|
+
# Use HTTP timeout slightly more than async response / long polling timeout
|
|
71
|
+
return (self.config["timeoutDeltaMillis"] + self.config["asyncResponseTimeout"]) / 1000
|
|
72
|
+
|
|
73
|
+
def _get_topics(self, topic_names, process_variables, variables):
|
|
74
|
+
topics = []
|
|
75
|
+
for topic in str_to_list(topic_names):
|
|
76
|
+
topics.append({
|
|
77
|
+
"topicName": topic,
|
|
78
|
+
"lockDuration": self.config["lockDuration"],
|
|
79
|
+
"processVariables": process_variables if process_variables else {},
|
|
80
|
+
# Enables Camunda Extension Properties
|
|
81
|
+
"includeExtensionProperties": self.config.get("includeExtensionProperties") or False,
|
|
82
|
+
"deserializeValues": self.config["deserializeValues"],
|
|
83
|
+
"variables": variables
|
|
84
|
+
})
|
|
85
|
+
return topics
|
|
86
|
+
|
|
87
|
+
async def complete(self, task_id, global_variables, local_variables=None):
|
|
88
|
+
url = self.get_task_complete_url(task_id)
|
|
89
|
+
|
|
90
|
+
body = {
|
|
91
|
+
"workerId": self.worker_id,
|
|
92
|
+
"variables": Variables.format(global_variables),
|
|
93
|
+
"localVariables": Variables.format(local_variables)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async with httpx.AsyncClient() as client:
|
|
97
|
+
response = await client.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
|
|
98
|
+
raise_exception_if_not_ok(response)
|
|
99
|
+
return response.status_code == HTTPStatus.NO_CONTENT
|
|
100
|
+
|
|
101
|
+
def get_task_complete_url(self, task_id):
|
|
102
|
+
return f"{self.external_task_base_url}/{task_id}/complete"
|
|
103
|
+
|
|
104
|
+
async def failure(self, task_id, error_message, error_details, retries, retry_timeout):
|
|
105
|
+
url = self.get_task_failure_url(task_id)
|
|
106
|
+
logger.info(f"Setting retries to: {retries} for task: {task_id}")
|
|
107
|
+
body = {
|
|
108
|
+
"workerId": self.worker_id,
|
|
109
|
+
"errorMessage": error_message,
|
|
110
|
+
"retries": retries,
|
|
111
|
+
"retryTimeout": retry_timeout,
|
|
112
|
+
}
|
|
113
|
+
if error_details:
|
|
114
|
+
body["errorDetails"] = error_details
|
|
115
|
+
|
|
116
|
+
async with httpx.AsyncClient() as client:
|
|
117
|
+
response = await client.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
|
|
118
|
+
raise_exception_if_not_ok(response)
|
|
119
|
+
return response.status_code == HTTPStatus.NO_CONTENT
|
|
120
|
+
|
|
121
|
+
def get_task_failure_url(self, task_id):
|
|
122
|
+
return f"{self.external_task_base_url}/{task_id}/failure"
|
|
123
|
+
|
|
124
|
+
async def bpmn_failure(self, task_id, error_code, error_message, variables=None):
|
|
125
|
+
url = self.get_task_bpmn_error_url(task_id)
|
|
126
|
+
|
|
127
|
+
body = {
|
|
128
|
+
"workerId": self.worker_id,
|
|
129
|
+
"errorCode": error_code,
|
|
130
|
+
"errorMessage": error_message,
|
|
131
|
+
"variables": Variables.format(variables),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if self.is_debug:
|
|
135
|
+
self._log_with_context(f"Trying to report BPMN error with request payload: {body}")
|
|
136
|
+
|
|
137
|
+
async with httpx.AsyncClient() as client:
|
|
138
|
+
response = await client.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
return response.status_code == HTTPStatus.NO_CONTENT
|
|
141
|
+
|
|
142
|
+
def get_task_bpmn_error_url(self, task_id):
|
|
143
|
+
return f"{self.external_task_base_url}/{task_id}/bpmnError"
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def auth_basic(self) -> dict:
|
|
147
|
+
if not self.config.get("auth_basic") or not isinstance(self.config.get("auth_basic"), dict):
|
|
148
|
+
return {}
|
|
149
|
+
token = AuthBasic(**self.config.get("auth_basic").copy()).token
|
|
150
|
+
return {"Authorization": token}
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def auth_bearer(self) -> dict:
|
|
154
|
+
if not self.config.get("auth_bearer") or not isinstance(self.config.get("auth_bearer"), dict):
|
|
155
|
+
return {}
|
|
156
|
+
token = AuthBearer(access_token=self.config["auth_bearer"]).access_token
|
|
157
|
+
return {"Authorization": token}
|
|
158
|
+
|
|
159
|
+
def _get_headers(self):
|
|
160
|
+
headers = {
|
|
161
|
+
"Content-Type": "application/json"
|
|
162
|
+
}
|
|
163
|
+
if self.auth_basic:
|
|
164
|
+
headers.update(self.auth_basic)
|
|
165
|
+
if self.auth_bearer:
|
|
166
|
+
headers.update(self.auth_bearer)
|
|
167
|
+
return headers
|
|
168
|
+
|
|
169
|
+
def _log_with_context(self, msg, log_level='info', **kwargs):
|
|
170
|
+
context = {"WORKER_ID": self.worker_id}
|
|
171
|
+
log_with_context(msg, context=context, log_level=log_level, **kwargs)
|