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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from operaton.external_task.external_task import ExternalTask
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExternalTaskTest(TestCase):
|
|
7
|
+
|
|
8
|
+
def test_external_task_creation_from_context(self):
|
|
9
|
+
context = {
|
|
10
|
+
"id": "123", "workerId": "321", "topicName": "my_topic", "tenantId": "tenant1",
|
|
11
|
+
"processInstanceId": "processInstanceId1",
|
|
12
|
+
"variables": {
|
|
13
|
+
"applicationId": {
|
|
14
|
+
"type": "String",
|
|
15
|
+
"value": "appId987",
|
|
16
|
+
"valueInfo": {}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
task = ExternalTask(context=context)
|
|
21
|
+
|
|
22
|
+
self.assertEqual("123", task.get_task_id())
|
|
23
|
+
self.assertEqual("321", task.get_worker_id())
|
|
24
|
+
self.assertEqual("my_topic", task.get_topic_name())
|
|
25
|
+
self.assertEqual("tenant1", task.get_tenant_id())
|
|
26
|
+
self.assertEqual("processInstanceId1", task.get_process_instance_id())
|
|
27
|
+
self.assertDictEqual({"applicationId": "appId987"}, task.get_variables())
|
|
28
|
+
self.assertEqual("empty_task_result", str(task.get_task_result()))
|
|
29
|
+
|
|
30
|
+
def test_complete_returns_success_task_result(self):
|
|
31
|
+
task = ExternalTask(context={})
|
|
32
|
+
task_result = task.complete({})
|
|
33
|
+
|
|
34
|
+
self.assertEqual(task, task_result.get_task())
|
|
35
|
+
self.assertEqual(task_result, task.get_task_result())
|
|
36
|
+
|
|
37
|
+
self.assertTrue(task_result.is_success())
|
|
38
|
+
self.assertFalse(task_result.is_failure())
|
|
39
|
+
self.assertFalse(task_result.is_bpmn_error())
|
|
40
|
+
|
|
41
|
+
def test_failure_returns_failure_task_result(self):
|
|
42
|
+
task = ExternalTask(context={})
|
|
43
|
+
task_result = task.failure(error_message="unknown error", error_details="error details here",
|
|
44
|
+
max_retries=3, retry_timeout=1000)
|
|
45
|
+
|
|
46
|
+
self.assertEqual(task, task_result.get_task())
|
|
47
|
+
self.assertEqual(task_result, task.get_task_result())
|
|
48
|
+
|
|
49
|
+
self.assertFalse(task_result.is_success())
|
|
50
|
+
self.assertTrue(task_result.is_failure())
|
|
51
|
+
self.assertFalse(task_result.is_bpmn_error())
|
|
52
|
+
|
|
53
|
+
self.assertEqual("unknown error", task_result.error_message)
|
|
54
|
+
self.assertEqual("error details here", task_result.error_details)
|
|
55
|
+
self.assertEqual(3, task_result.retries)
|
|
56
|
+
self.assertEqual(1000, task_result.retry_timeout)
|
|
57
|
+
|
|
58
|
+
def test_bpmn_error_returns_bpmn_error_task_result(self):
|
|
59
|
+
task = ExternalTask(context={})
|
|
60
|
+
task_result = task.bpmn_error(error_code="bpmn_error_code_1", error_message="bpmn error")
|
|
61
|
+
|
|
62
|
+
self.assertEqual(task, task_result.get_task())
|
|
63
|
+
self.assertEqual(task_result, task.get_task_result())
|
|
64
|
+
|
|
65
|
+
self.assertFalse(task_result.is_success())
|
|
66
|
+
self.assertFalse(task_result.is_failure())
|
|
67
|
+
self.assertTrue(task_result.is_bpmn_error())
|
|
68
|
+
|
|
69
|
+
self.assertEqual("bpmn_error_code_1", task_result.bpmn_error_code)
|
|
70
|
+
|
|
71
|
+
def test_task_with_retries_returns_failure_task_result_with_decremented_retries(self):
|
|
72
|
+
retries = 3
|
|
73
|
+
task = ExternalTask(context={"retries": retries})
|
|
74
|
+
task_result = task.failure(error_message="unknown error", error_details="error details here",
|
|
75
|
+
max_retries=10, retry_timeout=1000)
|
|
76
|
+
|
|
77
|
+
self.assertEqual(retries - 1, task_result.retries)
|
|
78
|
+
|
|
79
|
+
def test_get_variable_returns_none_for_missing_variable(self):
|
|
80
|
+
task = ExternalTask(context={})
|
|
81
|
+
variable = task.get_variable("var_name")
|
|
82
|
+
self.assertIsNone(variable)
|
|
83
|
+
|
|
84
|
+
def test_get_variable_returns_value_for_variable_present(self):
|
|
85
|
+
task = ExternalTask(context={"variables": {"var_name": {"value": 1}}})
|
|
86
|
+
variable = task.get_variable("var_name")
|
|
87
|
+
self.assertEqual(1, variable)
|
|
88
|
+
|
|
89
|
+
def test_get_variable_returns_with_meta(self):
|
|
90
|
+
task = ExternalTask(context={"variables": {"var_name": {"value": 1}}})
|
|
91
|
+
variable = task.get_variable("var_name", True)
|
|
92
|
+
self.assertEqual({"value": 1}, variable)
|
|
93
|
+
|
|
94
|
+
def test_get_variable_returns_without_meta(self):
|
|
95
|
+
task = ExternalTask(context={"variables": {"var_name": {"value": 1}}})
|
|
96
|
+
variable = task.get_variable("var_name", False)
|
|
97
|
+
self.assertEqual(1, variable)
|
|
98
|
+
|
|
99
|
+
def test_get_property_returns_value_for_property_present(self):
|
|
100
|
+
task = ExternalTask(context={"extensionProperties": {"var1":"one","var2":"two"}})
|
|
101
|
+
prop = task.get_extension_property("var1")
|
|
102
|
+
self.assertEqual("one", prop)
|
|
103
|
+
|
|
104
|
+
def test_str(self):
|
|
105
|
+
task = ExternalTask(context={"variables": {"var_name": {"value": 1}}})
|
|
106
|
+
self.assertEqual("{'variables': {'var_name': {'value': 1}}}", str(task))
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import collections
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from unittest import TestCase
|
|
5
|
+
|
|
6
|
+
import responses
|
|
7
|
+
|
|
8
|
+
from operaton.client.external_task_client import ExternalTaskClient
|
|
9
|
+
from operaton.external_task.external_task import TaskResult, ExternalTask
|
|
10
|
+
from operaton.external_task.external_task_executor import ExternalTaskExecutor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExternalTaskExecutorTest(TestCase):
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def task_success_action(task):
|
|
17
|
+
output_vars = {"var1": 1, "var2": "value", "var3": True}
|
|
18
|
+
return TaskResult.success(task, output_vars)
|
|
19
|
+
|
|
20
|
+
@responses.activate
|
|
21
|
+
def test_task_complete(self):
|
|
22
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
23
|
+
output_vars = {"var1": 1, "var2": "value", "var3": True}
|
|
24
|
+
expected_task_result = TaskResult.success(task, output_vars)
|
|
25
|
+
|
|
26
|
+
external_task_client = ExternalTaskClient(worker_id=1)
|
|
27
|
+
responses.add(
|
|
28
|
+
responses.POST,
|
|
29
|
+
external_task_client.get_task_complete_url(task.get_task_id()),
|
|
30
|
+
status=HTTPStatus.NO_CONTENT
|
|
31
|
+
)
|
|
32
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client)
|
|
33
|
+
|
|
34
|
+
actual_task_result = executor.execute_task(task, self.task_success_action)
|
|
35
|
+
self.assertEqual(str(expected_task_result), str(actual_task_result))
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def task_failure_action(task):
|
|
39
|
+
return TaskResult.failure(task, error_message="unknown task failure", error_details="unknown error",
|
|
40
|
+
retries=3, retry_timeout=30000)
|
|
41
|
+
|
|
42
|
+
@responses.activate
|
|
43
|
+
def test_task_failure(self):
|
|
44
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
45
|
+
expected_task_result = TaskResult.failure(
|
|
46
|
+
task,
|
|
47
|
+
error_message="unknown task failure",
|
|
48
|
+
error_details="unknown error",
|
|
49
|
+
retries=3,
|
|
50
|
+
retry_timeout=30000
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
external_task_client = ExternalTaskClient(worker_id=1)
|
|
54
|
+
responses.add(
|
|
55
|
+
responses.POST,
|
|
56
|
+
external_task_client.get_task_failure_url(task.get_task_id()),
|
|
57
|
+
status=HTTPStatus.NO_CONTENT
|
|
58
|
+
)
|
|
59
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client)
|
|
60
|
+
|
|
61
|
+
actual_task_result = executor.execute_task(task, self.task_failure_action)
|
|
62
|
+
self.assertEqual(str(expected_task_result), str(actual_task_result))
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def task_bpmn_error_action(task):
|
|
66
|
+
return TaskResult.bpmn_error(task, error_code="bpmn_err_code_1", error_message="bpmn error")
|
|
67
|
+
|
|
68
|
+
@responses.activate
|
|
69
|
+
def test_task_bpmn_error(self):
|
|
70
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
71
|
+
expected_task_result = TaskResult.bpmn_error(task, error_code="bpmn_err_code_1", error_message="bpmn error")
|
|
72
|
+
|
|
73
|
+
external_task_client = ExternalTaskClient(worker_id=1)
|
|
74
|
+
responses.add(
|
|
75
|
+
responses.POST,
|
|
76
|
+
external_task_client.get_task_bpmn_error_url(task.get_task_id()),
|
|
77
|
+
status=HTTPStatus.NO_CONTENT
|
|
78
|
+
)
|
|
79
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client)
|
|
80
|
+
|
|
81
|
+
actual_task_result = executor.execute_task(task, self.task_bpmn_error_action)
|
|
82
|
+
self.assertEqual(str(expected_task_result), str(actual_task_result))
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def task_result_not_complete_failure_bpmn_error(task):
|
|
86
|
+
return TaskResult.empty_task_result(task)
|
|
87
|
+
|
|
88
|
+
def test_task_result_not_complete_failure_bpmn_error_raises_exception(self):
|
|
89
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
90
|
+
external_task_client = ExternalTaskClient(worker_id=1)
|
|
91
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=external_task_client)
|
|
92
|
+
|
|
93
|
+
with self.assertRaises(Exception) as exception_ctx:
|
|
94
|
+
executor.execute_task(task, self.task_result_not_complete_failure_bpmn_error)
|
|
95
|
+
|
|
96
|
+
self.assertEqual(
|
|
97
|
+
"task result for task_id=1 must be either complete/failure/BPMNError",
|
|
98
|
+
str(exception_ctx.exception)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@responses.activate
|
|
102
|
+
def test_execute_task_raises_exception_raised_when_updating_status_in_engine(self):
|
|
103
|
+
client = ExternalTaskClient(worker_id=1)
|
|
104
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
105
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=client)
|
|
106
|
+
|
|
107
|
+
TaskResultStatusInput = collections.namedtuple(
|
|
108
|
+
'TaskResultStatusInput',
|
|
109
|
+
['task_status', 'task_action', 'task_status_url', 'error_message']
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
task_result_tests = [
|
|
113
|
+
TaskResultStatusInput(
|
|
114
|
+
"complete", self.task_success_action,
|
|
115
|
+
client.get_task_complete_url(task.get_task_id()),
|
|
116
|
+
"cannot update task status to complete"
|
|
117
|
+
),
|
|
118
|
+
TaskResultStatusInput(
|
|
119
|
+
"failure", self.task_failure_action,
|
|
120
|
+
client.get_task_failure_url(task.get_task_id()),
|
|
121
|
+
"cannot update task status to failure"
|
|
122
|
+
),
|
|
123
|
+
TaskResultStatusInput(
|
|
124
|
+
"bpmn_error", self.task_bpmn_error_action,
|
|
125
|
+
client.get_task_bpmn_error_url(task.get_task_id()),
|
|
126
|
+
"cannot update task status to BPMN err"
|
|
127
|
+
)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for task_result_test in task_result_tests:
|
|
131
|
+
with self.subTest(task_result_test.task_status):
|
|
132
|
+
responses.add(
|
|
133
|
+
responses.POST,
|
|
134
|
+
task_result_test.task_status_url,
|
|
135
|
+
body=Exception(task_result_test.error_message)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
with self.assertRaises(Exception) as exception_ctx:
|
|
139
|
+
executor.execute_task(task, task_result_test.task_action)
|
|
140
|
+
|
|
141
|
+
self.assertEqual(task_result_test.error_message, str(exception_ctx.exception))
|
|
142
|
+
|
|
143
|
+
@responses.activate
|
|
144
|
+
def test_execute_task_raises_exception_if_engine_returns_http_status_other_than_no_content_204(self):
|
|
145
|
+
client = ExternalTaskClient(worker_id=1)
|
|
146
|
+
task = ExternalTask({"id": "1", "topicName": "my_topic"})
|
|
147
|
+
executor = ExternalTaskExecutor(worker_id=1, external_task_client=client)
|
|
148
|
+
|
|
149
|
+
TaskResultStatusInput = collections.namedtuple('TaskResultStatusInput',
|
|
150
|
+
['task_status', 'task_action', 'task_status_url',
|
|
151
|
+
'http_status_code', 'expected_error_message'])
|
|
152
|
+
|
|
153
|
+
task_result_tests = [
|
|
154
|
+
TaskResultStatusInput(
|
|
155
|
+
"complete", self.task_success_action,
|
|
156
|
+
client.get_task_complete_url(task.get_task_id()), HTTPStatus.OK,
|
|
157
|
+
'Not able to mark complete for task_id=1 for topic=my_topic, worker_id=1'
|
|
158
|
+
),
|
|
159
|
+
TaskResultStatusInput(
|
|
160
|
+
"failure", self.task_failure_action,
|
|
161
|
+
client.get_task_failure_url(task.get_task_id()), HTTPStatus.CREATED,
|
|
162
|
+
'Not able to mark failure for task_id=1 for topic=my_topic, worker_id=1'
|
|
163
|
+
),
|
|
164
|
+
TaskResultStatusInput(
|
|
165
|
+
"bpmn_error", self.task_bpmn_error_action,
|
|
166
|
+
client.get_task_bpmn_error_url(task.get_task_id()), HTTPStatus.ACCEPTED,
|
|
167
|
+
'Not able to mark BPMN Error for task_id=1 for topic=my_topic, worker_id=1'
|
|
168
|
+
)
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
for task_result_test in task_result_tests:
|
|
172
|
+
with self.subTest(task_result_test.task_status):
|
|
173
|
+
responses.add(
|
|
174
|
+
responses.POST, task_result_test.task_status_url,
|
|
175
|
+
status=task_result_test.http_status_code
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
with self.assertRaises(Exception) as exception_ctx:
|
|
179
|
+
executor.execute_task(task, task_result_test.task_action)
|
|
180
|
+
|
|
181
|
+
self.assertEqual(task_result_test.expected_error_message, str(exception_ctx.exception))
|
|
182
|
+
|
|
183
|
+
def test_strip_long_variables(self):
|
|
184
|
+
variables = {
|
|
185
|
+
"var0": "string",
|
|
186
|
+
"var1": {"value": "string"},
|
|
187
|
+
"var2": {"value": 1},
|
|
188
|
+
"var3": {"value": "{\"key\": \"value\"}", "type": "Json"},
|
|
189
|
+
"var4": {"value": base64.encodebytes(b"string"), "type": "Bytes"},
|
|
190
|
+
"var5": {"value": "some file content", "type": "File"},
|
|
191
|
+
}
|
|
192
|
+
cleaned = ExternalTaskExecutor("worker-1", None)._strip_long_variables(variables)
|
|
193
|
+
self.assertEqual({
|
|
194
|
+
"var0": "string",
|
|
195
|
+
"var1": {"value": "string"},
|
|
196
|
+
"var2": {"value": 1},
|
|
197
|
+
"var3": {"value": "{\"key\": \"value\"}", "type": "Json"},
|
|
198
|
+
"var4": {"value": "...", "type": "Bytes"},
|
|
199
|
+
"var5": {"value": "...", "type": "File"},
|
|
200
|
+
}, cleaned)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from unittest import mock, TestCase
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import responses
|
|
6
|
+
|
|
7
|
+
from operaton.client.external_task_client import ExternalTaskClient
|
|
8
|
+
from operaton.external_task.external_task import TaskResult, ExternalTask
|
|
9
|
+
from operaton.external_task.external_task_worker import ExternalTaskWorker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExternalTaskWorkerTest(TestCase):
|
|
13
|
+
|
|
14
|
+
@responses.activate
|
|
15
|
+
@patch('operaton.client.external_task_client.ExternalTaskClient.complete')
|
|
16
|
+
def test_fetch_and_execute_calls_task_action_for_each_task_fetched(self, _):
|
|
17
|
+
external_task_client = ExternalTaskClient(worker_id=0)
|
|
18
|
+
resp_payload = [{
|
|
19
|
+
"activityId": "anActivityId",
|
|
20
|
+
"activityInstanceId": "anActivityInstanceId",
|
|
21
|
+
"errorMessage": "anErrorMessage",
|
|
22
|
+
"errorDetails": "anErrorDetails",
|
|
23
|
+
"executionId": "anExecutionId",
|
|
24
|
+
"id": "anExternalTaskId",
|
|
25
|
+
"lockExpirationTime": "2015-10-06T16:34:42",
|
|
26
|
+
"processDefinitionId": "aProcessDefinitionId",
|
|
27
|
+
"processDefinitionKey": "aProcessDefinitionKey",
|
|
28
|
+
"processInstanceId": "aProcessInstanceId",
|
|
29
|
+
"tenantId": None,
|
|
30
|
+
"retries": 3,
|
|
31
|
+
"workerId": "aWorkerId",
|
|
32
|
+
"priority": 4,
|
|
33
|
+
"topicName": "createOrder",
|
|
34
|
+
"variables": {
|
|
35
|
+
"orderId": {
|
|
36
|
+
"type": "String",
|
|
37
|
+
"value": "1234",
|
|
38
|
+
"valueInfo": {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"activityId": "anActivityId",
|
|
44
|
+
"activityInstanceId": "anActivityInstanceId",
|
|
45
|
+
"errorMessage": "anErrorMessage",
|
|
46
|
+
"errorDetails": "anotherErrorDetails",
|
|
47
|
+
"executionId": "anExecutionId",
|
|
48
|
+
"id": "anExternalTaskId",
|
|
49
|
+
"lockExpirationTime": "2015-10-06T16:34:42",
|
|
50
|
+
"processDefinitionId": "aProcessDefinitionId",
|
|
51
|
+
"processDefinitionKey": "aProcessDefinitionKey",
|
|
52
|
+
"processInstanceId": "aProcessInstanceId",
|
|
53
|
+
"tenantId": None,
|
|
54
|
+
"retries": 3,
|
|
55
|
+
"workerId": "aWorkerId",
|
|
56
|
+
"priority": 0,
|
|
57
|
+
"topicName": "createOrder",
|
|
58
|
+
"variables": {
|
|
59
|
+
"orderId": {
|
|
60
|
+
"type": "String",
|
|
61
|
+
"value": "3456",
|
|
62
|
+
"valueInfo": {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}]
|
|
66
|
+
responses.add(responses.POST, external_task_client.get_fetch_and_lock_url(),
|
|
67
|
+
status=HTTPStatus.OK, json=resp_payload)
|
|
68
|
+
|
|
69
|
+
worker = ExternalTaskWorker(worker_id=0)
|
|
70
|
+
mock_action = mock.Mock()
|
|
71
|
+
task = ExternalTask({"id": "anExternalTaskId", "workerId": "aWorkerId", "topicName": "createOrder"})
|
|
72
|
+
mock_action.return_value = TaskResult.success(task=task, global_variables={})
|
|
73
|
+
|
|
74
|
+
worker.fetch_and_execute("my_topic", mock_action)
|
|
75
|
+
self.assertEqual(2, mock_action.call_count)
|
|
76
|
+
|
|
77
|
+
@responses.activate
|
|
78
|
+
def test_fetch_and_execute_raises_exception_if_task_action_raises_exception(self):
|
|
79
|
+
external_task_client = ExternalTaskClient(worker_id=0)
|
|
80
|
+
resp_payload = [{
|
|
81
|
+
"activityId": "anActivityId",
|
|
82
|
+
"activityInstanceId": "anActivityInstanceId",
|
|
83
|
+
"errorMessage": "anErrorMessage",
|
|
84
|
+
"errorDetails": "anErrorDetails",
|
|
85
|
+
"executionId": "anExecutionId",
|
|
86
|
+
"id": "anExternalTaskId",
|
|
87
|
+
"lockExpirationTime": "2015-10-06T16:34:42",
|
|
88
|
+
"processDefinitionId": "aProcessDefinitionId",
|
|
89
|
+
"processDefinitionKey": "aProcessDefinitionKey",
|
|
90
|
+
"processInstanceId": "aProcessInstanceId",
|
|
91
|
+
"tenantId": None,
|
|
92
|
+
"retries": 3,
|
|
93
|
+
"workerId": "aWorkerId",
|
|
94
|
+
"priority": 4,
|
|
95
|
+
"topicName": "createOrder",
|
|
96
|
+
"variables": {
|
|
97
|
+
"orderId": {
|
|
98
|
+
"type": "String",
|
|
99
|
+
"value": "1234",
|
|
100
|
+
"valueInfo": {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}]
|
|
104
|
+
responses.add(responses.POST, external_task_client.get_fetch_and_lock_url(),
|
|
105
|
+
status=HTTPStatus.OK, json=resp_payload)
|
|
106
|
+
|
|
107
|
+
worker = ExternalTaskWorker(worker_id=0)
|
|
108
|
+
mock_action = mock.Mock()
|
|
109
|
+
mock_action.side_effect = Exception("error executing task action")
|
|
110
|
+
|
|
111
|
+
with self.assertRaises(Exception) as exception_ctx:
|
|
112
|
+
worker.fetch_and_execute("my_topic", mock_action)
|
|
113
|
+
|
|
114
|
+
self.assertEqual("error executing task action", str(exception_ctx.exception))
|
|
115
|
+
|
|
116
|
+
@responses.activate
|
|
117
|
+
def test_fetch_and_execute_raises_exception_if_no_tasks_found(self):
|
|
118
|
+
external_task_client = ExternalTaskClient(worker_id=0)
|
|
119
|
+
resp_payload = []
|
|
120
|
+
responses.add(responses.POST, external_task_client.get_fetch_and_lock_url(),
|
|
121
|
+
status=HTTPStatus.OK, json=resp_payload)
|
|
122
|
+
|
|
123
|
+
worker = ExternalTaskWorker(worker_id=0)
|
|
124
|
+
mock_action = mock.Mock()
|
|
125
|
+
process_variables = {"var1": "value1", "var2": "value2"}
|
|
126
|
+
with self.assertRaises(Exception) as context:
|
|
127
|
+
worker.fetch_and_execute("my_topic", mock_action, process_variables)
|
|
128
|
+
|
|
129
|
+
self.assertEqual(f"no External Task found for Topics: my_topic, Process variables: {process_variables}",
|
|
130
|
+
str(context.exception))
|
|
131
|
+
|
|
132
|
+
@responses.activate
|
|
133
|
+
@patch('time.sleep', return_value=None)
|
|
134
|
+
def test_fetch_and_execute_safe_raises_exception_sleep_is_called(self, mock_time_sleep):
|
|
135
|
+
external_task_client = ExternalTaskClient(worker_id=0)
|
|
136
|
+
responses.add(responses.POST, external_task_client.get_fetch_and_lock_url(),
|
|
137
|
+
status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
138
|
+
|
|
139
|
+
sleep_seconds = 100
|
|
140
|
+
worker = ExternalTaskWorker(worker_id=0, config={"sleepSeconds": sleep_seconds})
|
|
141
|
+
mock_action = mock.Mock()
|
|
142
|
+
|
|
143
|
+
worker._fetch_and_execute_safe("my_topic", mock_action)
|
|
144
|
+
|
|
145
|
+
self.assertEqual(0, mock_action.call_count)
|
|
146
|
+
self.assertEqual(1, mock_time_sleep.call_count)
|
|
147
|
+
mock_time_sleep.assert_called_with(sleep_seconds)
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from operaton.client.engine_client import EngineClient, ENGINE_LOCAL_BASE_URL
|
|
6
|
+
from operaton.utils.response_utils import raise_exception_if_not_ok
|
|
7
|
+
from operaton.utils.utils import join
|
|
8
|
+
from operaton.variables.variables import Variables
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessDefinitionClient(EngineClient):
|
|
14
|
+
def __init__(self, engine_base_url=ENGINE_LOCAL_BASE_URL, config=None):
|
|
15
|
+
super().__init__(engine_base_url, config=config)
|
|
16
|
+
|
|
17
|
+
def get_process_definitions(
|
|
18
|
+
self,
|
|
19
|
+
process_key,
|
|
20
|
+
version_tag,
|
|
21
|
+
tenant_ids,
|
|
22
|
+
sort_by="version",
|
|
23
|
+
sort_order="desc",
|
|
24
|
+
offset=0,
|
|
25
|
+
limit=1,
|
|
26
|
+
):
|
|
27
|
+
url = self.get_process_definitions_url()
|
|
28
|
+
url_params = self.get_process_definitions_url_params(
|
|
29
|
+
process_key, version_tag, tenant_ids, sort_by, sort_order, offset, limit
|
|
30
|
+
)
|
|
31
|
+
response = requests.get(url, headers=self._get_headers(), params=url_params)
|
|
32
|
+
raise_exception_if_not_ok(response)
|
|
33
|
+
return response.json()
|
|
34
|
+
|
|
35
|
+
def get_process_definitions_url(self):
|
|
36
|
+
return f"{self.engine_base_url}/process-definition"
|
|
37
|
+
|
|
38
|
+
def get_process_definitions_url_params(
|
|
39
|
+
self,
|
|
40
|
+
process_key,
|
|
41
|
+
version_tag=None,
|
|
42
|
+
tenant_ids=None,
|
|
43
|
+
sort_by="version",
|
|
44
|
+
sort_order="desc",
|
|
45
|
+
offset=0,
|
|
46
|
+
limit=1,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
offset starts with zero
|
|
50
|
+
sort_order can be "asc" or "desc
|
|
51
|
+
"""
|
|
52
|
+
url_params = {
|
|
53
|
+
"key": process_key,
|
|
54
|
+
"versionTagLike": f"{version_tag}%" if version_tag else None,
|
|
55
|
+
"tenantIdIn": join(tenant_ids, ","),
|
|
56
|
+
"sortBy": sort_by,
|
|
57
|
+
"sortOrder": sort_order,
|
|
58
|
+
"firstResult": offset,
|
|
59
|
+
"maxResults": limit,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
url_params = {k: v for k, v in url_params.items() if v is not None and v != ""}
|
|
63
|
+
|
|
64
|
+
return url_params
|
|
65
|
+
|
|
66
|
+
def start_process_by_version(
|
|
67
|
+
self, process_key, version_tag, variables, tenant_id=None, business_key=None
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
Start a process instance with the process_key and specified version tag and variables passed.
|
|
71
|
+
If multiple versions with same version tag found, it triggers the latest one
|
|
72
|
+
:param process_key: Mandatory
|
|
73
|
+
:param version_tag:
|
|
74
|
+
:param variables: Mandatory - can be empty dict
|
|
75
|
+
:param tenant_id: Optional
|
|
76
|
+
:param business_key: Optional
|
|
77
|
+
:return: response json
|
|
78
|
+
"""
|
|
79
|
+
tenant_ids = [tenant_id] if tenant_id else []
|
|
80
|
+
process_definitions = self.get_process_definitions(
|
|
81
|
+
process_key,
|
|
82
|
+
version_tag,
|
|
83
|
+
tenant_ids,
|
|
84
|
+
sort_by="version",
|
|
85
|
+
sort_order="desc",
|
|
86
|
+
offset=0,
|
|
87
|
+
limit=1,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if len(process_definitions) == 0:
|
|
91
|
+
raise Exception(
|
|
92
|
+
f"cannot start process because no process definitions found "
|
|
93
|
+
f"for process_key: {process_key}, version_tag: {version_tag} and tenant_id: {tenant_id}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
process_definition_id = process_definitions[0]["id"]
|
|
97
|
+
version = process_definitions[0]["version"]
|
|
98
|
+
if len(process_definitions) > 1:
|
|
99
|
+
logger.info(
|
|
100
|
+
f"multiple process definitions found for process_key: {process_key}, "
|
|
101
|
+
f"version_tag: {version_tag} and tenant_id: {tenant_id}, "
|
|
102
|
+
f"using latest process_definition_id: {process_definition_id} with version: {version}"
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
logger.info(
|
|
106
|
+
f"exactly one process definition found for process_key: {process_key}, "
|
|
107
|
+
f"version_tag: {version_tag} and tenant_id: {tenant_id}, "
|
|
108
|
+
f"using process_definition_id: {process_definition_id} with version: {version}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
url = self.get_start_process_url(process_definition_id)
|
|
112
|
+
body = {"variables": Variables.format(variables)}
|
|
113
|
+
if business_key:
|
|
114
|
+
body["businessKey"] = business_key
|
|
115
|
+
|
|
116
|
+
response = requests.post(url, headers=self._get_headers(), json=body)
|
|
117
|
+
raise_exception_if_not_ok(response)
|
|
118
|
+
return response.json()
|
|
119
|
+
|
|
120
|
+
def get_start_process_url(self, process_definition_id):
|
|
121
|
+
return (
|
|
122
|
+
f"{self.engine_base_url}/process-definition/{process_definition_id}/start"
|
|
123
|
+
)
|
|
File without changes
|