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.
Files changed (66) hide show
  1. examples/__init__.py +0 -0
  2. examples/bpmn_error_example.py +75 -0
  3. examples/correlate_message.py +11 -0
  4. examples/event_subprocess_example.py +50 -0
  5. examples/examples_auth_basic/__init__.py +0 -0
  6. examples/examples_auth_basic/fetch_and_execute.py +31 -0
  7. examples/examples_auth_basic/get_process_instance.py +12 -0
  8. examples/examples_auth_basic/start_process.py +15 -0
  9. examples/examples_auth_basic/task_handler_example.py +44 -0
  10. examples/fetch_and_execute.py +30 -0
  11. examples/get_process_instance.py +12 -0
  12. examples/retry_task_example.py +58 -0
  13. examples/start_process.py +14 -0
  14. examples/task_handler_example.py +44 -0
  15. examples/tasks_example.py +36 -0
  16. operaton/__init__.py +0 -0
  17. operaton/client/__init__.py +0 -0
  18. operaton/client/async_external_task_client.py +171 -0
  19. operaton/client/engine_client.py +180 -0
  20. operaton/client/external_task_client.py +166 -0
  21. operaton/client/tests/__init__.py +0 -0
  22. operaton/client/tests/test_async_external_task_client.py +128 -0
  23. operaton/client/tests/test_async_external_task_client_auth.py +42 -0
  24. operaton/client/tests/test_async_external_task_client_bearer.py +43 -0
  25. operaton/client/tests/test_engine_client.py +228 -0
  26. operaton/client/tests/test_engine_client_auth.py +231 -0
  27. operaton/client/tests/test_engine_client_bearer.py +237 -0
  28. operaton/client/tests/test_external_task_client.py +17 -0
  29. operaton/client/tests/test_external_task_client_auth.py +19 -0
  30. operaton/client/tests/test_external_task_client_bearer.py +24 -0
  31. operaton/external_task/__init__.py +0 -0
  32. operaton/external_task/async_external_task_executor.py +91 -0
  33. operaton/external_task/async_external_task_worker.py +181 -0
  34. operaton/external_task/external_task.py +173 -0
  35. operaton/external_task/external_task_executor.py +88 -0
  36. operaton/external_task/external_task_worker.py +92 -0
  37. operaton/external_task/tests/__init__.py +0 -0
  38. operaton/external_task/tests/test_async_external_task_executor.py +139 -0
  39. operaton/external_task/tests/test_async_external_task_worker.py +129 -0
  40. operaton/external_task/tests/test_external_task.py +106 -0
  41. operaton/external_task/tests/test_external_task_executor.py +200 -0
  42. operaton/external_task/tests/test_external_task_worker.py +147 -0
  43. operaton/process_definition/__init__.py +0 -0
  44. operaton/process_definition/process_definition_client.py +123 -0
  45. operaton/process_definition/tests/__init__.py +0 -0
  46. operaton/process_definition/tests/test_process_definition_client.py +181 -0
  47. operaton/utils/__init__.py +0 -0
  48. operaton/utils/auth_basic.py +28 -0
  49. operaton/utils/auth_bearer.py +28 -0
  50. operaton/utils/log_utils.py +31 -0
  51. operaton/utils/response_utils.py +35 -0
  52. operaton/utils/tests/test_auth_basic.py +30 -0
  53. operaton/utils/tests/test_auth_bearer.py +27 -0
  54. operaton/utils/tests/test_response_utils.py +43 -0
  55. operaton/utils/tests/test_utils.py +21 -0
  56. operaton/utils/utils.py +14 -0
  57. operaton/variables/__init__.py +0 -0
  58. operaton/variables/properties.py +27 -0
  59. operaton/variables/tests/test_properties.py +20 -0
  60. operaton/variables/tests/test_variables.py +60 -0
  61. operaton/variables/variables.py +45 -0
  62. operaton_external_task_client_python3-1.0.0.dist-info/METADATA +258 -0
  63. operaton_external_task_client_python3-1.0.0.dist-info/RECORD +66 -0
  64. operaton_external_task_client_python3-1.0.0.dist-info/WHEEL +5 -0
  65. operaton_external_task_client_python3-1.0.0.dist-info/licenses/LICENSE +201 -0
  66. operaton_external_task_client_python3-1.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,180 @@
1
+ import base64
2
+ import logging
3
+ from http import HTTPStatus
4
+
5
+ import requests
6
+
7
+ from operaton.utils.response_utils import raise_exception_if_not_ok
8
+ from operaton.utils.utils import join
9
+ from operaton.utils.auth_basic import AuthBasic
10
+ from operaton.utils.auth_bearer import AuthBearer
11
+ from operaton.variables.variables import Variables
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ ENGINE_LOCAL_BASE_URL = "http://localhost:8080/engine-rest"
16
+
17
+
18
+ class EngineClient:
19
+
20
+ def __init__(self, engine_base_url=ENGINE_LOCAL_BASE_URL, config=None):
21
+ config = config if config is not None else {}
22
+ self.config = config.copy()
23
+ self.engine_base_url = engine_base_url
24
+
25
+ def get_start_process_instance_url(self, process_key, tenant_id=None):
26
+ if tenant_id:
27
+ return f"{self.engine_base_url}/process-definition/key/{process_key}/tenant-id/{tenant_id}/start"
28
+ return f"{self.engine_base_url}/process-definition/key/{process_key}/start"
29
+
30
+ def start_process(self, process_key, variables, tenant_id=None, business_key=None):
31
+ """
32
+ Start a process instance with the process_key and variables passed.
33
+ :param process_key: Mandatory
34
+ :param variables: Mandatory - can be empty dict
35
+ :param tenant_id: Optional
36
+ :param business_key: Optional
37
+ :return: response json
38
+ """
39
+ url = self.get_start_process_instance_url(process_key, tenant_id)
40
+ body = {
41
+ "variables": Variables.format(variables)
42
+ }
43
+ if business_key:
44
+ body["businessKey"] = business_key
45
+
46
+ response = requests.post(url, headers=self._get_headers(), json=body)
47
+ raise_exception_if_not_ok(response)
48
+ return response.json()
49
+
50
+ def get_process_instance(self, process_key=None, variables=frozenset([]), tenant_ids=frozenset([])):
51
+ url = f"{self.engine_base_url}/process-instance"
52
+ url_params = self.__get_process_instance_url_params(process_key, tenant_ids, variables)
53
+ response = requests.get(url, headers=self._get_headers(), params=url_params)
54
+ raise_exception_if_not_ok(response)
55
+ return response.json()
56
+
57
+ @staticmethod
58
+ def __get_process_instance_url_params(process_key, tenant_ids, variables):
59
+ url_params = {}
60
+ if process_key:
61
+ url_params["processDefinitionKey"] = process_key
62
+ var_filter = join(variables, ',')
63
+ if var_filter:
64
+ url_params["variables"] = var_filter
65
+ tenant_ids_filter = join(tenant_ids, ',')
66
+ if tenant_ids_filter:
67
+ url_params["tenantIdIn"] = tenant_ids_filter
68
+ return url_params
69
+
70
+ @property
71
+ def auth_basic(self) -> dict:
72
+ if not self.config.get("auth_basic") or not isinstance(self.config.get("auth_basic"), dict):
73
+ return {}
74
+ token = AuthBasic(**self.config.get("auth_basic").copy()).token
75
+ return {"Authorization": token}
76
+
77
+ @property
78
+ def auth_bearer(self) -> dict:
79
+ if not self.config.get("auth_bearer") or not isinstance(self.config.get("auth_bearer"), dict):
80
+ return {}
81
+ token = AuthBearer(access_token=self.config["auth_bearer"]).access_token
82
+ return {"Authorization": token}
83
+
84
+ def _get_headers(self):
85
+ headers = {
86
+ "Content-Type": "application/json"
87
+ }
88
+ if self.auth_basic:
89
+ headers.update(self.auth_basic)
90
+ if self.auth_bearer:
91
+ headers.update(self.auth_bearer)
92
+ return headers
93
+
94
+ def correlate_message(self, message_name, process_instance_id=None, tenant_id=None, business_key=None,
95
+ process_variables=None):
96
+ """
97
+ Correlates a message to the process engine to either trigger a message start event or
98
+ an intermediate message catching event.
99
+ :param message_name:
100
+ :param process_instance_id:
101
+ :param tenant_id:
102
+ :param business_key:
103
+ :param process_variables:
104
+ :return: response json
105
+ """
106
+ url = f"{self.engine_base_url}/message"
107
+ body = {
108
+ "messageName": message_name,
109
+ "resultEnabled": True,
110
+ "processVariables": Variables.format(process_variables) if process_variables else None,
111
+ "processInstanceId": process_instance_id,
112
+ "tenantId": tenant_id,
113
+ "withoutTenantId": not tenant_id,
114
+ "businessKey": business_key,
115
+ }
116
+
117
+ if process_instance_id:
118
+ body.pop("tenantId")
119
+ body.pop("withoutTenantId")
120
+
121
+ body = {k: v for k, v in body.items() if v is not None}
122
+
123
+ response = requests.post(url, headers=self._get_headers(), json=body)
124
+ raise_exception_if_not_ok(response)
125
+ return response.json()
126
+
127
+ def get_jobs(self,
128
+ offset: int,
129
+ limit: int,
130
+ tenant_ids=None,
131
+ with_failure=None,
132
+ process_instance_id=None,
133
+ task_name=None,
134
+ sort_by="jobDueDate",
135
+ sort_order="desc"):
136
+ # offset starts with zero
137
+ # sort_order can be "asc" or "desc
138
+
139
+ url = f"{self.engine_base_url}/job"
140
+ params = {
141
+ "firstResult": offset,
142
+ "maxResults": limit,
143
+ "sortBy": sort_by,
144
+ "sortOrder": sort_order,
145
+ }
146
+ if process_instance_id:
147
+ params["processInstanceId"] = process_instance_id
148
+ if task_name:
149
+ params["failedActivityId"] = task_name
150
+ if with_failure:
151
+ params["withException"] = "true"
152
+ if tenant_ids:
153
+ params["tenantIdIn"] = ','.join(tenant_ids)
154
+ response = requests.get(url, params=params, headers=self._get_headers())
155
+ raise_exception_if_not_ok(response)
156
+ return response.json()
157
+
158
+ def set_job_retry(self, job_id, retries=1):
159
+ url = f"{self.engine_base_url}/job/{job_id}/retries"
160
+ body = {"retries": retries}
161
+
162
+ response = requests.put(url, headers=self._get_headers(), json=body)
163
+ raise_exception_if_not_ok(response)
164
+ return response.status_code == HTTPStatus.NO_CONTENT
165
+
166
+ def get_process_instance_variable(self, process_instance_id, variable_name, with_meta=False):
167
+ url = f"{self.engine_base_url}/process-instance/{process_instance_id}/variables/{variable_name}"
168
+ response = requests.get(url, headers=self._get_headers())
169
+ raise_exception_if_not_ok(response)
170
+ resp_json = response.json()
171
+
172
+ url_with_data = f"{url}/data"
173
+ response = requests.get(url_with_data, headers=self._get_headers())
174
+ raise_exception_if_not_ok(response)
175
+
176
+ decoded_value = base64.encodebytes(response.content).decode("utf-8")
177
+
178
+ if with_meta:
179
+ return dict(resp_json, value=decoded_value)
180
+ return decoded_value
@@ -0,0 +1,166 @@
1
+ import logging
2
+ from http import HTTPStatus
3
+
4
+ import requests
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 ExternalTaskClient:
18
+ default_config = {
19
+ "maxTasks": 1,
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
+ 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": self.config["maxTasks"],
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
+ response = requests.post(url, headers=self._get_headers(), json=body, timeout=http_timeout_seconds)
60
+ raise_exception_if_not_ok(response)
61
+
62
+ resp_json = response.json()
63
+ if self.is_debug:
64
+ self._log_with_context(f"fetch and lock response json: {resp_json} for request: {body}")
65
+ return response.json()
66
+
67
+ def __get_fetch_and_lock_http_timeout_seconds(self):
68
+ # use HTTP timeout slightly more than async Response / long polling timeout
69
+ return (self.config["timeoutDeltaMillis"] + self.config["asyncResponseTimeout"]) / 1000
70
+
71
+ def _get_topics(self, topic_names, process_variables, variables):
72
+ topics = []
73
+ for topic in str_to_list(topic_names):
74
+ topics.append({
75
+ "topicName": topic,
76
+ "lockDuration": self.config["lockDuration"],
77
+ "processVariables": process_variables if process_variables else {},
78
+ # enables Camunda Extension Properties
79
+ "includeExtensionProperties": self.config.get("includeExtensionProperties") or False,
80
+ "deserializeValues": self.config["deserializeValues"],
81
+ "variables": variables
82
+ })
83
+ return topics
84
+
85
+ def complete(self, task_id, global_variables, local_variables=None):
86
+ url = self.get_task_complete_url(task_id)
87
+
88
+ body = {
89
+ "workerId": self.worker_id,
90
+ "variables": Variables.format(global_variables),
91
+ "localVariables": Variables.format(local_variables)
92
+ }
93
+
94
+ response = requests.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
95
+ raise_exception_if_not_ok(response)
96
+ return response.status_code == HTTPStatus.NO_CONTENT
97
+
98
+ def get_task_complete_url(self, task_id):
99
+ return f"{self.external_task_base_url}/{task_id}/complete"
100
+
101
+ def failure(self, task_id, error_message, error_details, retries, retry_timeout):
102
+ url = self.get_task_failure_url(task_id)
103
+ logger.info(f"setting retries to: {retries} for task: {task_id}")
104
+ body = {
105
+ "workerId": self.worker_id,
106
+ "errorMessage": error_message,
107
+ "retries": retries,
108
+ "retryTimeout": retry_timeout,
109
+ }
110
+ if error_details:
111
+ body["errorDetails"] = error_details
112
+
113
+ response = requests.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
114
+ raise_exception_if_not_ok(response)
115
+ return response.status_code == HTTPStatus.NO_CONTENT
116
+
117
+ def get_task_failure_url(self, task_id):
118
+ return f"{self.external_task_base_url}/{task_id}/failure"
119
+
120
+ def bpmn_failure(self, task_id, error_code, error_message, variables=None):
121
+ url = self.get_task_bpmn_error_url(task_id)
122
+
123
+ body = {
124
+ "workerId": self.worker_id,
125
+ "errorCode": error_code,
126
+ "errorMessage": error_message,
127
+ "variables": Variables.format(variables),
128
+ }
129
+
130
+ if self.is_debug:
131
+ self._log_with_context(f"trying to report bpmn error with request payload: {body}")
132
+
133
+ resp = requests.post(url, headers=self._get_headers(), json=body, timeout=self.http_timeout_seconds)
134
+ resp.raise_for_status()
135
+ return resp.status_code == HTTPStatus.NO_CONTENT
136
+
137
+ def get_task_bpmn_error_url(self, task_id):
138
+ return f"{self.external_task_base_url}/{task_id}/bpmnError"
139
+
140
+ @property
141
+ def auth_basic(self) -> dict:
142
+ if not self.config.get("auth_basic") or not isinstance(self.config.get("auth_basic"), dict):
143
+ return {}
144
+ token = AuthBasic(**self.config.get("auth_basic").copy()).token
145
+ return {"Authorization": token}
146
+
147
+ @property
148
+ def auth_bearer(self) -> dict:
149
+ if not self.config.get("auth_bearer") or not isinstance(self.config.get("auth_bearer"), dict):
150
+ return {}
151
+ token = AuthBearer(access_token=self.config["auth_bearer"]).access_token
152
+ return {"Authorization": token}
153
+
154
+ def _get_headers(self):
155
+ headers = {
156
+ "Content-Type": "application/json"
157
+ }
158
+ if self.auth_basic:
159
+ headers.update(self.auth_basic)
160
+ if self.auth_bearer:
161
+ headers.update(self.auth_bearer)
162
+ return headers
163
+
164
+ def _log_with_context(self, msg, log_level='info', **kwargs):
165
+ context = {"WORKER_ID": self.worker_id}
166
+ log_with_context(msg, context=context, log_level=log_level, **kwargs)
File without changes
@@ -0,0 +1,128 @@
1
+ import unittest
2
+ from http import HTTPStatus
3
+ from unittest.mock import patch, AsyncMock
4
+
5
+ import httpx
6
+
7
+ # Adjust the import based on your actual module path
8
+ from operaton.client.async_external_task_client import AsyncExternalTaskClient, ENGINE_LOCAL_BASE_URL
9
+
10
+
11
+ class AsyncExternalTaskClientTest(unittest.IsolatedAsyncioTestCase):
12
+ """
13
+ Tests for async_external_task_client.py
14
+ """
15
+
16
+ def setUp(self):
17
+ # Common setup if needed
18
+ self.default_worker_id = 1
19
+ self.default_engine_url = ENGINE_LOCAL_BASE_URL
20
+
21
+ async def test_creation_with_no_debug_config(self):
22
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {})
23
+ self.assertFalse(client.is_debug)
24
+ self.assertFalse(client.config.get("isDebug"))
25
+ # Check default_config merges:
26
+ self.assertEqual(client.config["maxConcurrentTasks"], 10)
27
+ self.assertEqual(client.config["lockDuration"], 300000)
28
+
29
+ async def test_creation_with_debug_config(self):
30
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {"isDebug": True})
31
+ self.assertTrue(client.is_debug)
32
+ self.assertTrue(client.config.get("isDebug"))
33
+
34
+ @patch("httpx.AsyncClient.post")
35
+ async def test_fetch_and_lock_success(self, mock_post):
36
+ # Provide actual JSON as bytes
37
+ content = b'[{"id": "someExternalTaskId", "topicName": "topicA"}]'
38
+ mock_post.return_value = httpx.Response(
39
+ status_code=200,
40
+ request=httpx.Request("POST", "http://example.com"),
41
+ content=content
42
+ )
43
+
44
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {})
45
+ # Perform call
46
+ tasks = await client.fetch_and_lock("topicA")
47
+
48
+ # Assertions
49
+ expected_url = f"{ENGINE_LOCAL_BASE_URL}/external-task/fetchAndLock"
50
+ self.assertEqual([{"id": "someExternalTaskId", "topicName": "topicA"}], tasks)
51
+ mock_post.assert_awaited_once() # Check post was awaited exactly once
52
+ args, kwargs = mock_post.call_args
53
+ self.assertEqual(expected_url, args[0], "Expected correct fetchAndLock endpoint URL")
54
+ # You could also check the payload or headers here:
55
+ self.assertIn("json", kwargs)
56
+ self.assertEqual(kwargs["json"]["workerId"], "1") # str(worker_id)
57
+
58
+ @patch("httpx.AsyncClient.post")
59
+ async def test_fetch_and_lock_server_error(self, mock_post):
60
+ # Create a real httpx.Response with status=500
61
+ server_err_resp = httpx.Response(
62
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
63
+ request=httpx.Request("POST", "http://example.com/external-task/fetchAndLock"),
64
+ content=b"Internal Server Error"
65
+ )
66
+ # Each call to mock_post() returns this real response object
67
+ mock_post.return_value = server_err_resp
68
+
69
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {})
70
+
71
+ # Now we expect an exception
72
+ with self.assertRaises(httpx.HTTPStatusError) as ctx:
73
+ await client.fetch_and_lock("topicA")
74
+
75
+ # Optional: confirm the error message
76
+ self.assertIn("500 Internal Server Error", str(ctx.exception))
77
+
78
+ @patch("httpx.AsyncClient.post", new_callable=AsyncMock)
79
+ async def test_complete_success(self, mock_post):
80
+ mock_post.return_value.status_code = HTTPStatus.NO_CONTENT
81
+
82
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {})
83
+ result = await client.complete("myTaskId", {"globalVar": 1})
84
+
85
+ self.assertTrue(result)
86
+ mock_post.assert_awaited_once()
87
+ complete_url = f"{ENGINE_LOCAL_BASE_URL}/external-task/myTaskId/complete"
88
+ self.assertEqual(complete_url, mock_post.call_args[0][0])
89
+
90
+ @patch("httpx.AsyncClient.post", new_callable=AsyncMock)
91
+ async def test_failure_with_error_details(self, mock_post):
92
+ mock_post.return_value.status_code = HTTPStatus.NO_CONTENT
93
+
94
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {})
95
+ result = await client.failure(
96
+ task_id="myTaskId",
97
+ error_message="some error",
98
+ error_details="stacktrace info",
99
+ retries=3,
100
+ retry_timeout=10000
101
+ )
102
+
103
+ self.assertTrue(result)
104
+ mock_post.assert_awaited_once()
105
+ failure_url = f"{ENGINE_LOCAL_BASE_URL}/external-task/myTaskId/failure"
106
+ self.assertEqual(failure_url, mock_post.call_args[0][0])
107
+ self.assertIn("errorDetails", mock_post.call_args[1]["json"])
108
+
109
+ @patch("httpx.AsyncClient.post", new_callable=AsyncMock)
110
+ async def test_bpmn_failure_success(self, mock_post):
111
+ mock_post.return_value.status_code = HTTPStatus.NO_CONTENT
112
+
113
+ client = AsyncExternalTaskClient(self.default_worker_id, self.default_engine_url, {"isDebug": True})
114
+ result = await client.bpmn_failure(
115
+ task_id="myTaskId",
116
+ error_code="BPMN_ERROR",
117
+ error_message="an example BPMN error",
118
+ variables={"foo": "bar"}
119
+ )
120
+
121
+ self.assertTrue(result)
122
+ mock_post.assert_awaited_once()
123
+ bpmn_url = f"{ENGINE_LOCAL_BASE_URL}/external-task/myTaskId/bpmnError"
124
+ args, kwargs = mock_post.call_args
125
+ self.assertEqual(bpmn_url, args[0])
126
+ self.assertEqual(kwargs["json"]["errorCode"], "BPMN_ERROR")
127
+ self.assertTrue(client.is_debug) # Confirm the debug flag is set
128
+
@@ -0,0 +1,42 @@
1
+ import unittest
2
+ from http import HTTPStatus
3
+ from unittest.mock import AsyncMock, patch
4
+
5
+ from operaton.client.async_external_task_client import AsyncExternalTaskClient
6
+ from operaton.client.engine_client import ENGINE_LOCAL_BASE_URL
7
+
8
+
9
+ class AsyncExternalTaskClientAuthTest(unittest.IsolatedAsyncioTestCase):
10
+ async def test_auth_basic_fetch_and_lock_no_debug(self):
11
+ with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
12
+ mock_post.return_value.status_code = HTTPStatus.OK
13
+ mock_post.return_value.json.return_value = []
14
+
15
+ client = AsyncExternalTaskClient(
16
+ 1,
17
+ ENGINE_LOCAL_BASE_URL,
18
+ {"auth_basic": {"username": "demo", "password": "demo"}}
19
+ )
20
+ await client.fetch_and_lock("someTopic")
21
+
22
+ # Confirm "Authorization" header is present
23
+ headers_used = mock_post.call_args[1]["headers"]
24
+ self.assertIn("Authorization", headers_used)
25
+ self.assertTrue(headers_used["Authorization"].startswith("Basic "))
26
+
27
+ async def test_auth_basic_fetch_and_lock_with_debug(self):
28
+ with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
29
+ mock_post.return_value.status_code = HTTPStatus.OK
30
+ mock_post.return_value.json.return_value = []
31
+
32
+ client = AsyncExternalTaskClient(
33
+ 1,
34
+ ENGINE_LOCAL_BASE_URL,
35
+ {"auth_basic": {"username": "demo", "password": "demo"}, "isDebug": True}
36
+ )
37
+ await client.fetch_and_lock("someTopic")
38
+
39
+ # Confirm "Authorization" header is present
40
+ headers_used = mock_post.call_args[1]["headers"]
41
+ self.assertIn("Authorization", headers_used)
42
+ self.assertTrue(headers_used["Authorization"].startswith("Basic "))
@@ -0,0 +1,43 @@
1
+ import unittest
2
+ from http import HTTPStatus
3
+ from unittest.mock import patch, AsyncMock
4
+
5
+ from operaton.client.async_external_task_client import AsyncExternalTaskClient
6
+ from operaton.client.engine_client import ENGINE_LOCAL_BASE_URL
7
+
8
+
9
+ class AsyncExternalTaskClientAuthTest(unittest.IsolatedAsyncioTestCase):
10
+
11
+ async def test_auth_bearer_fetch_and_lock_no_debug(self):
12
+ token = "some.super.long.jwt"
13
+ with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
14
+ mock_post.return_value.status_code = HTTPStatus.OK
15
+ mock_post.return_value.json.return_value = []
16
+
17
+ client = AsyncExternalTaskClient(
18
+ 1,
19
+ ENGINE_LOCAL_BASE_URL,
20
+ {"auth_bearer": {"access_token": token}}
21
+ )
22
+ await client.fetch_and_lock("someTopic")
23
+
24
+ headers_used = mock_post.call_args[1]["headers"]
25
+ self.assertIn("Authorization", headers_used)
26
+ self.assertEqual(f"Bearer {token}", headers_used["Authorization"])
27
+
28
+ async def test_auth_bearer_fetch_and_lock_with_debug(self):
29
+ token = "some.super.long.jwt"
30
+ with patch("httpx.AsyncClient.post", new_callable=AsyncMock) as mock_post:
31
+ mock_post.return_value.status_code = HTTPStatus.OK
32
+ mock_post.return_value.json.return_value = []
33
+
34
+ client = AsyncExternalTaskClient(
35
+ 1,
36
+ ENGINE_LOCAL_BASE_URL,
37
+ {"auth_bearer": {"access_token": token}, "isDebug": True}
38
+ )
39
+ await client.fetch_and_lock("someTopic")
40
+
41
+ headers_used = mock_post.call_args[1]["headers"]
42
+ self.assertIn("Authorization", headers_used)
43
+ self.assertEqual(f"Bearer {token}", headers_used["Authorization"])