dex-python-sdk 0.0.1__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.
- dex/__init__.py +14 -0
- dex/client.py +568 -0
- dex/client_options.py +34 -0
- dex/command_request.py +130 -0
- dex/command_results.py +116 -0
- dex/communication.py +141 -0
- dex/communication_schema.py +68 -0
- dex/data_attributes.py +75 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +305 -0
- dex/dexpb/dex_pb2.pyi +1273 -0
- dex/dexpb/dex_pb2_grpc.py +1126 -0
- dex/errors.py +132 -0
- dex/object_encoder.py +818 -0
- dex/persistence.py +94 -0
- dex/persistence_options.py +24 -0
- dex/persistence_schema.py +58 -0
- dex/py.typed +1 -0
- dex/registry.py +209 -0
- dex/reset_workflow_type_and_options.py +77 -0
- dex/rpc.py +100 -0
- dex/search_attributes.py +189 -0
- dex/state_decision.py +161 -0
- dex/state_execution_locals.py +71 -0
- dex/state_movement.py +123 -0
- dex/state_schema.py +54 -0
- dex/stop_workflow_options.py +23 -0
- dex/tests/__init__.py +84 -0
- dex/tests/dex-service-env/.env +7 -0
- dex/tests/dex-service-env/docker-compose-init.sh +44 -0
- dex/tests/dex-service-env/docker-compose.yml +87 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +6 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -0
- dex/tests/test_abnormal_exit_workflow.py +48 -0
- dex/tests/test_basic_workflow.py +75 -0
- dex/tests/test_conditional_complete.py +56 -0
- dex/tests/test_describe_workflow.py +45 -0
- dex/tests/test_empty_data_decodes_properly.py +79 -0
- dex/tests/test_internal_channel.py +33 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +46 -0
- dex/tests/test_persistence_data_attributes.py +67 -0
- dex/tests/test_persistence_search_attributes.py +132 -0
- dex/tests/test_persistence_state_execution_locals.py +43 -0
- dex/tests/test_rpc.py +69 -0
- dex/tests/test_rpc_with_memo.py +200 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +122 -0
- dex/tests/test_signal.py +56 -0
- dex/tests/test_skip_wait_until.py +85 -0
- dex/tests/test_state_failure_recovery.py +33 -0
- dex/tests/test_timer.py +40 -0
- dex/tests/test_wait_for_state_execution_completion.py +58 -0
- dex/tests/test_workflow_errors.py +92 -0
- dex/tests/test_workflow_state_options.py +123 -0
- dex/tests/test_workflow_state_options_override.py +49 -0
- dex/tests/worker_server.py +74 -0
- dex/tests/workflows/abnormal_exit_workflow.py +48 -0
- dex/tests/workflows/basic_workflow.py +69 -0
- dex/tests/workflows/conditional_complete_workflow.py +101 -0
- dex/tests/workflows/describe_workflow.py +52 -0
- dex/tests/workflows/empty_data_workflow.py +51 -0
- dex/tests/workflows/internal_channel_workflow.py +137 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +108 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +285 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +104 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +167 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +69 -0
- dex/tests/workflows/recovery_workflow.py +90 -0
- dex/tests/workflows/rpc_memo_workflow.py +238 -0
- dex/tests/workflows/rpc_workflow.py +125 -0
- dex/tests/workflows/state_options_override_workflow.py +100 -0
- dex/tests/workflows/state_options_workflow.py +92 -0
- dex/tests/workflows/timer_workflow.py +52 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +77 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +78 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +53 -0
- dex/tests/workflows/wait_signal_workflow.py +154 -0
- dex/type_store.py +105 -0
- dex/unregistered_client.py +595 -0
- dex/utils/__init__.py +14 -0
- dex/utils/dex_typing.py +31 -0
- dex/utils/persistence_utils.py +37 -0
- dex/worker_service.py +441 -0
- dex/workflow.py +86 -0
- dex/workflow_context.py +50 -0
- dex/workflow_info.py +21 -0
- dex/workflow_options.py +79 -0
- dex/workflow_state.py +134 -0
- dex/workflow_state_options.py +160 -0
- dex_python_sdk-0.0.1.dist-info/METADATA +140 -0
- dex_python_sdk-0.0.1.dist-info/RECORD +93 -0
- dex_python_sdk-0.0.1.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
dex/worker_service.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import traceback
|
|
16
|
+
import typing
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import List, Union
|
|
19
|
+
|
|
20
|
+
from dex.command_request import _to_idl_command_request
|
|
21
|
+
from dex.command_results import from_idl_command_results
|
|
22
|
+
from dex.communication import Communication
|
|
23
|
+
from dex.data_attributes import DataAttributes
|
|
24
|
+
from dex.dex_api.models import (
|
|
25
|
+
EncodedObject,
|
|
26
|
+
KeyValue,
|
|
27
|
+
SearchAttribute,
|
|
28
|
+
SearchAttributeValueType,
|
|
29
|
+
WorkflowStateExecuteRequest,
|
|
30
|
+
WorkflowStateExecuteResponse,
|
|
31
|
+
WorkflowStateWaitUntilRequest,
|
|
32
|
+
WorkflowStateWaitUntilResponse,
|
|
33
|
+
WorkflowWorkerRpcRequest,
|
|
34
|
+
WorkflowWorkerRpcResponse,
|
|
35
|
+
)
|
|
36
|
+
from dex.dex_api.types import Unset
|
|
37
|
+
from dex.object_encoder import ObjectEncoder
|
|
38
|
+
from dex.persistence import Persistence
|
|
39
|
+
from dex.registry import Registry
|
|
40
|
+
from dex.search_attributes import SearchAttributes
|
|
41
|
+
from dex.state_decision import StateDecision, _to_idl_state_decision
|
|
42
|
+
from dex.state_execution_locals import StateExecutionLocals
|
|
43
|
+
from dex.utils.dex_typing import assert_not_unset, unset_to_none
|
|
44
|
+
from dex.workflow_context import WorkflowContext, _from_idl_context
|
|
45
|
+
from dex.workflow_state import get_input_type
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WorkerOptions:
|
|
50
|
+
object_encoder: ObjectEncoder
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
default_worker_options = WorkerOptions(ObjectEncoder.default)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WorkerService:
|
|
57
|
+
api_path_workflow_state_wait_until: typing.ClassVar[str] = (
|
|
58
|
+
"/api/v1/workflowState/start"
|
|
59
|
+
)
|
|
60
|
+
api_path_workflow_state_execute: typing.ClassVar[str] = (
|
|
61
|
+
"/api/v1/workflowState/decide"
|
|
62
|
+
)
|
|
63
|
+
api_path_workflow_worker_rpc: typing.ClassVar[str] = "/api/v1/workflowWorker/rpc"
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self, registry: Registry, options: WorkerOptions = default_worker_options
|
|
67
|
+
):
|
|
68
|
+
self._registry = registry
|
|
69
|
+
self._options = options
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def handle_worker_error(exception: Exception):
|
|
73
|
+
"""
|
|
74
|
+
Handle the exception/error of worker so that Temporal/Cadence WebUI can show the error nicely.
|
|
75
|
+
Example usage (in Flask):
|
|
76
|
+
@_flask_app.errorhandler(Exception)
|
|
77
|
+
def internal_error(exception):
|
|
78
|
+
return _worker_service.handle_worker_error(exception), 500
|
|
79
|
+
"""
|
|
80
|
+
stacktrace = traceback.format_exc()
|
|
81
|
+
index = max(0, stacktrace.find("dex-python-sdk/dex/worker_service.py"))
|
|
82
|
+
return "WorkerExecutionError: {0}; StackTrace:{1}".format(
|
|
83
|
+
exception, stacktrace[index:]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def handle_workflow_worker_rpc(
|
|
87
|
+
self,
|
|
88
|
+
request: WorkflowWorkerRpcRequest,
|
|
89
|
+
) -> WorkflowWorkerRpcResponse:
|
|
90
|
+
wf_type = request.workflow_type
|
|
91
|
+
rpc_info = self._registry.get_rpc_infos(wf_type)[request.rpc_name]
|
|
92
|
+
|
|
93
|
+
internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
|
|
94
|
+
signal_channel_types = self._registry.get_signal_channel_types(wf_type)
|
|
95
|
+
data_attributes_types = self._registry.get_data_attribute_types(wf_type)
|
|
96
|
+
|
|
97
|
+
context = _from_idl_context(request.context)
|
|
98
|
+
_input = self._options.object_encoder.decode(
|
|
99
|
+
unset_to_none(request.input_), rpc_info.input_type
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
|
|
103
|
+
{}
|
|
104
|
+
)
|
|
105
|
+
if not isinstance(request.data_attributes, Unset):
|
|
106
|
+
current_data_attributes = {
|
|
107
|
+
assert_not_unset(attr.key): unset_to_none(attr.value)
|
|
108
|
+
for attr in request.data_attributes
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
data_attributes = DataAttributes(
|
|
112
|
+
data_attributes_types,
|
|
113
|
+
self._options.object_encoder,
|
|
114
|
+
current_data_attributes,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
search_attributes_types = self._registry.get_search_attribute_types(wf_type)
|
|
118
|
+
search_attributes = SearchAttributes(
|
|
119
|
+
search_attributes_types, unset_to_none(request.search_attributes)
|
|
120
|
+
)
|
|
121
|
+
state_execution_locals = StateExecutionLocals(
|
|
122
|
+
to_map(None), self._options.object_encoder
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
persistence = Persistence(
|
|
126
|
+
data_attributes, search_attributes, state_execution_locals
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
communication = Communication(
|
|
130
|
+
internal_channel_types,
|
|
131
|
+
signal_channel_types,
|
|
132
|
+
self._options.object_encoder,
|
|
133
|
+
unset_to_none(request.internal_channel_infos),
|
|
134
|
+
unset_to_none(request.signal_channel_infos),
|
|
135
|
+
)
|
|
136
|
+
params: typing.Any = []
|
|
137
|
+
if rpc_info.params_order is not None:
|
|
138
|
+
for param_type in rpc_info.params_order:
|
|
139
|
+
if param_type == Persistence:
|
|
140
|
+
params.append(persistence)
|
|
141
|
+
elif param_type == Communication:
|
|
142
|
+
params.append(communication)
|
|
143
|
+
elif param_type == WorkflowContext:
|
|
144
|
+
params.append(context)
|
|
145
|
+
else:
|
|
146
|
+
params.append(_input)
|
|
147
|
+
|
|
148
|
+
output = rpc_info.method_func(*params)
|
|
149
|
+
|
|
150
|
+
pubs = communication.get_to_publishing_internal_channel()
|
|
151
|
+
response = WorkflowWorkerRpcResponse(
|
|
152
|
+
output=self._options.object_encoder.encode(output)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if len(pubs) > 0:
|
|
156
|
+
response.publish_to_inter_state_channel = pubs
|
|
157
|
+
if len(data_attributes.get_updated_values_to_return()) > 0:
|
|
158
|
+
response.upsert_data_attributes = [
|
|
159
|
+
KeyValue(k, v)
|
|
160
|
+
for (k, v) in data_attributes.get_updated_values_to_return().items()
|
|
161
|
+
]
|
|
162
|
+
upsert_sas = _create_upsert_search_attributes(
|
|
163
|
+
search_attributes_types,
|
|
164
|
+
search_attributes.get_upsert_to_server_int64_attribute_map(),
|
|
165
|
+
search_attributes.get_upsert_to_server_string_attribute_map(),
|
|
166
|
+
search_attributes.get_upsert_to_server_bool_attribute_map(),
|
|
167
|
+
search_attributes.get_upsert_to_server_double_attribute_map(),
|
|
168
|
+
search_attributes.get_upsert_to_server_string_array_attribute_map(),
|
|
169
|
+
)
|
|
170
|
+
if upsert_sas:
|
|
171
|
+
response.upsert_search_attributes = upsert_sas
|
|
172
|
+
record_events = state_execution_locals.get_record_events()
|
|
173
|
+
if len(record_events) > 0:
|
|
174
|
+
response.record_events = record_events
|
|
175
|
+
if len(communication.get_to_trigger_state_movements()) > 0:
|
|
176
|
+
movements = communication.get_to_trigger_state_movements()
|
|
177
|
+
decision = StateDecision.multi_next_states(*movements)
|
|
178
|
+
response.state_decision = _to_idl_state_decision(
|
|
179
|
+
decision,
|
|
180
|
+
wf_type,
|
|
181
|
+
self._registry,
|
|
182
|
+
self._options.object_encoder,
|
|
183
|
+
)
|
|
184
|
+
return response
|
|
185
|
+
|
|
186
|
+
def handle_workflow_state_wait_until(
|
|
187
|
+
self,
|
|
188
|
+
request: WorkflowStateWaitUntilRequest,
|
|
189
|
+
) -> WorkflowStateWaitUntilResponse:
|
|
190
|
+
wf_type = request.workflow_type
|
|
191
|
+
state = self._registry.get_workflow_state_with_check(
|
|
192
|
+
wf_type, request.workflow_state_id
|
|
193
|
+
)
|
|
194
|
+
internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
|
|
195
|
+
signal_channel_types = self._registry.get_signal_channel_types(wf_type)
|
|
196
|
+
data_attributes_types = self._registry.get_data_attribute_types(wf_type)
|
|
197
|
+
|
|
198
|
+
context = _from_idl_context(request.context)
|
|
199
|
+
_input = self._options.object_encoder.decode(
|
|
200
|
+
unset_to_none(request.state_input), get_input_type(state)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
|
|
204
|
+
{}
|
|
205
|
+
)
|
|
206
|
+
if not isinstance(request.data_objects, Unset):
|
|
207
|
+
current_data_attributes = {
|
|
208
|
+
assert_not_unset(attr.key): unset_to_none(attr.value)
|
|
209
|
+
for attr in request.data_objects
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
data_attributes = DataAttributes(
|
|
213
|
+
data_attributes_types,
|
|
214
|
+
self._options.object_encoder,
|
|
215
|
+
current_data_attributes,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
search_attributes_types = self._registry.get_search_attribute_types(wf_type)
|
|
219
|
+
search_attributes = SearchAttributes(
|
|
220
|
+
search_attributes_types, unset_to_none(request.search_attributes)
|
|
221
|
+
)
|
|
222
|
+
state_execution_locals = StateExecutionLocals(
|
|
223
|
+
to_map(None), self._options.object_encoder
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
persistence = Persistence(
|
|
227
|
+
data_attributes, search_attributes, state_execution_locals
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
communication = Communication(
|
|
231
|
+
internal_channel_types,
|
|
232
|
+
signal_channel_types,
|
|
233
|
+
self._options.object_encoder,
|
|
234
|
+
None,
|
|
235
|
+
None,
|
|
236
|
+
)
|
|
237
|
+
command_request = state.wait_until(context, _input, persistence, communication)
|
|
238
|
+
|
|
239
|
+
pubs = communication.get_to_publishing_internal_channel()
|
|
240
|
+
|
|
241
|
+
upsert_sas = _create_upsert_search_attributes(
|
|
242
|
+
search_attributes_types,
|
|
243
|
+
search_attributes.get_upsert_to_server_int64_attribute_map(),
|
|
244
|
+
search_attributes.get_upsert_to_server_string_attribute_map(),
|
|
245
|
+
search_attributes.get_upsert_to_server_bool_attribute_map(),
|
|
246
|
+
search_attributes.get_upsert_to_server_double_attribute_map(),
|
|
247
|
+
search_attributes.get_upsert_to_server_string_array_attribute_map(),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
upsert_state_locals = (
|
|
251
|
+
state_execution_locals.get_upsert_state_execution_local_attributes()
|
|
252
|
+
)
|
|
253
|
+
record_events = state_execution_locals.get_record_events()
|
|
254
|
+
|
|
255
|
+
response = WorkflowStateWaitUntilResponse(
|
|
256
|
+
command_request=_to_idl_command_request(command_request),
|
|
257
|
+
publish_to_inter_state_channel=pubs,
|
|
258
|
+
upsert_data_objects=[
|
|
259
|
+
KeyValue(k, v)
|
|
260
|
+
for (k, v) in data_attributes.get_updated_values_to_return().items()
|
|
261
|
+
],
|
|
262
|
+
upsert_state_locals=upsert_state_locals,
|
|
263
|
+
record_events=record_events,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if upsert_sas:
|
|
267
|
+
response.upsert_search_attributes = upsert_sas
|
|
268
|
+
|
|
269
|
+
return response
|
|
270
|
+
|
|
271
|
+
def handle_workflow_state_execute(
|
|
272
|
+
self,
|
|
273
|
+
request: WorkflowStateExecuteRequest,
|
|
274
|
+
) -> WorkflowStateExecuteResponse:
|
|
275
|
+
wf_type = request.workflow_type
|
|
276
|
+
state = self._registry.get_workflow_state_with_check(
|
|
277
|
+
wf_type, request.workflow_state_id
|
|
278
|
+
)
|
|
279
|
+
internal_channel_types = self._registry.get_internal_channel_type_store(wf_type)
|
|
280
|
+
signal_channel_types = self._registry.get_signal_channel_types(wf_type)
|
|
281
|
+
data_attributes_types = self._registry.get_data_attribute_types(wf_type)
|
|
282
|
+
context = _from_idl_context(request.context)
|
|
283
|
+
|
|
284
|
+
_input = self._options.object_encoder.decode(
|
|
285
|
+
unset_to_none(request.state_input), get_input_type(state)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
current_data_attributes: dict[str, typing.Union[EncodedObject, None, Unset]] = (
|
|
289
|
+
{}
|
|
290
|
+
)
|
|
291
|
+
if not isinstance(request.data_objects, Unset):
|
|
292
|
+
current_data_attributes = {
|
|
293
|
+
assert_not_unset(attr.key): unset_to_none(attr.value)
|
|
294
|
+
for attr in request.data_objects
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
data_attributes = DataAttributes(
|
|
298
|
+
data_attributes_types,
|
|
299
|
+
self._options.object_encoder,
|
|
300
|
+
current_data_attributes,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
search_attributes_types = self._registry.get_search_attribute_types(wf_type)
|
|
304
|
+
search_attributes = SearchAttributes(
|
|
305
|
+
search_attributes_types, unset_to_none(request.search_attributes)
|
|
306
|
+
)
|
|
307
|
+
state_execution_locals = StateExecutionLocals(
|
|
308
|
+
to_map(request.state_locals), self._options.object_encoder
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
persistence = Persistence(
|
|
312
|
+
data_attributes, search_attributes, state_execution_locals
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
communication = Communication(
|
|
316
|
+
internal_channel_types,
|
|
317
|
+
signal_channel_types,
|
|
318
|
+
self._options.object_encoder,
|
|
319
|
+
None,
|
|
320
|
+
None,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
command_results = from_idl_command_results(
|
|
324
|
+
request.command_results,
|
|
325
|
+
internal_channel_types,
|
|
326
|
+
signal_channel_types,
|
|
327
|
+
self._options.object_encoder,
|
|
328
|
+
)
|
|
329
|
+
decision = state.execute(
|
|
330
|
+
context, _input, command_results, persistence, communication
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
pubs = communication.get_to_publishing_internal_channel()
|
|
334
|
+
|
|
335
|
+
upsert_sas = _create_upsert_search_attributes(
|
|
336
|
+
search_attributes_types,
|
|
337
|
+
search_attributes.get_upsert_to_server_int64_attribute_map(),
|
|
338
|
+
search_attributes.get_upsert_to_server_string_attribute_map(),
|
|
339
|
+
search_attributes.get_upsert_to_server_bool_attribute_map(),
|
|
340
|
+
search_attributes.get_upsert_to_server_double_attribute_map(),
|
|
341
|
+
search_attributes.get_upsert_to_server_string_array_attribute_map(),
|
|
342
|
+
)
|
|
343
|
+
upsert_state_locals = (
|
|
344
|
+
state_execution_locals.get_upsert_state_execution_local_attributes()
|
|
345
|
+
)
|
|
346
|
+
record_events = state_execution_locals.get_record_events()
|
|
347
|
+
|
|
348
|
+
response = WorkflowStateExecuteResponse(
|
|
349
|
+
state_decision=_to_idl_state_decision(
|
|
350
|
+
decision,
|
|
351
|
+
wf_type,
|
|
352
|
+
self._registry,
|
|
353
|
+
self._options.object_encoder,
|
|
354
|
+
),
|
|
355
|
+
publish_to_inter_state_channel=pubs,
|
|
356
|
+
upsert_data_objects=[
|
|
357
|
+
KeyValue(k, v)
|
|
358
|
+
for (k, v) in data_attributes.get_updated_values_to_return().items()
|
|
359
|
+
],
|
|
360
|
+
upsert_state_locals=upsert_state_locals,
|
|
361
|
+
record_events=record_events,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if upsert_sas:
|
|
365
|
+
response.upsert_search_attributes = upsert_sas
|
|
366
|
+
|
|
367
|
+
return response
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _create_upsert_search_attributes(
|
|
371
|
+
type_map: dict[str, SearchAttributeValueType],
|
|
372
|
+
upsert_to_server_int64_attribute_map: dict[str, Union[int, None]],
|
|
373
|
+
upsert_to_server_keyword_attribute_map: dict[str, Union[str, None]],
|
|
374
|
+
upsert_to_server_bool_attribute_map: dict[str, Union[bool, None]],
|
|
375
|
+
upsert_to_server_double_attribute_map: dict[str, Union[float, None]],
|
|
376
|
+
upsert_to_server_string_array_attribute_map: dict[str, Union[list[str], None]],
|
|
377
|
+
):
|
|
378
|
+
sas: list[SearchAttribute] = []
|
|
379
|
+
for int_key, int_sa in upsert_to_server_int64_attribute_map.items():
|
|
380
|
+
sa = SearchAttribute(
|
|
381
|
+
key=int_key,
|
|
382
|
+
value_type=type_map[int_key],
|
|
383
|
+
)
|
|
384
|
+
if int_sa is not None:
|
|
385
|
+
sa.integer_value = int_sa
|
|
386
|
+
sas.append(sa)
|
|
387
|
+
|
|
388
|
+
for keyword_key, keyword_sa in upsert_to_server_keyword_attribute_map.items():
|
|
389
|
+
sa = SearchAttribute(
|
|
390
|
+
key=keyword_key,
|
|
391
|
+
value_type=type_map[keyword_key],
|
|
392
|
+
)
|
|
393
|
+
if keyword_sa is not None:
|
|
394
|
+
sa.string_value = keyword_sa
|
|
395
|
+
sas.append(sa)
|
|
396
|
+
|
|
397
|
+
for bool_key, bool_sa in upsert_to_server_bool_attribute_map.items():
|
|
398
|
+
sa = SearchAttribute(
|
|
399
|
+
key=bool_key,
|
|
400
|
+
value_type=type_map[bool_key],
|
|
401
|
+
)
|
|
402
|
+
if bool_sa is not None:
|
|
403
|
+
sa.bool_value = bool_sa
|
|
404
|
+
sas.append(sa)
|
|
405
|
+
|
|
406
|
+
for double_key, double_sa in upsert_to_server_double_attribute_map.items():
|
|
407
|
+
sa = SearchAttribute(
|
|
408
|
+
key=double_key,
|
|
409
|
+
value_type=type_map[double_key],
|
|
410
|
+
)
|
|
411
|
+
if double_sa is not None:
|
|
412
|
+
sa.double_value = double_sa
|
|
413
|
+
sas.append(sa)
|
|
414
|
+
|
|
415
|
+
for (
|
|
416
|
+
string_array_key,
|
|
417
|
+
string_array_sa,
|
|
418
|
+
) in upsert_to_server_string_array_attribute_map.items():
|
|
419
|
+
sa = SearchAttribute(
|
|
420
|
+
key=string_array_key,
|
|
421
|
+
value_type=type_map[string_array_key],
|
|
422
|
+
)
|
|
423
|
+
if string_array_sa is not None:
|
|
424
|
+
sa.string_array_value = string_array_sa
|
|
425
|
+
sas.append(sa)
|
|
426
|
+
|
|
427
|
+
return sas
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def to_map(
|
|
431
|
+
key_values: Union[None, Unset, List[KeyValue]],
|
|
432
|
+
) -> dict[str, Union[EncodedObject, Unset]]:
|
|
433
|
+
key_values = unset_to_none(key_values) or []
|
|
434
|
+
kvs: dict[str, Union[EncodedObject, Unset]] = {}
|
|
435
|
+
for kv in key_values:
|
|
436
|
+
k = unset_to_none(kv.key)
|
|
437
|
+
v = unset_to_none(kv.value)
|
|
438
|
+
if k and v:
|
|
439
|
+
kvs[k] = v
|
|
440
|
+
|
|
441
|
+
return kvs
|
dex/workflow.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from abc import ABC
|
|
16
|
+
|
|
17
|
+
from dex.communication_schema import CommunicationSchema
|
|
18
|
+
from dex.persistence_options import PersistenceOptions
|
|
19
|
+
from dex.persistence_schema import PersistenceSchema
|
|
20
|
+
from dex.state_schema import StateSchema
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ObjectWorkflow(ABC):
|
|
24
|
+
"""ObjectWorkflow is the interface to define a workflow definition.
|
|
25
|
+
ObjectWorkflow is a top level concept in Dex. Any object that is long-lasting
|
|
26
|
+
can be modeled as an ObjectWorkflow.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get_workflow_states(self) -> StateSchema:
|
|
30
|
+
"""
|
|
31
|
+
GetWorkflowStates defines the states of the workflow. A state represents
|
|
32
|
+
a step of the workflow state machine.
|
|
33
|
+
A state can execute some commands (signal/timer) and wait for result
|
|
34
|
+
See more details in the WorkflowState interface.
|
|
35
|
+
It can return an empty list, meaning no states.
|
|
36
|
+
There can be at most one startingState in the list.
|
|
37
|
+
If there is no startingState or with the default empty state list, the workflow
|
|
38
|
+
will not start any state execution after workflow stated. Application can still
|
|
39
|
+
use RPC to invoke new state execution in the future.
|
|
40
|
+
Returns:
|
|
41
|
+
A list of workflow state definitions. Default to empty.
|
|
42
|
+
"""
|
|
43
|
+
return StateSchema()
|
|
44
|
+
|
|
45
|
+
def get_persistence_schema(self) -> PersistenceSchema:
|
|
46
|
+
"""
|
|
47
|
+
GetPersistenceSchema defines all the persistence fields for this workflow, includes:
|
|
48
|
+
1. Data attributes
|
|
49
|
+
2. Search attributes
|
|
50
|
+
Data attributes can be read/upsert in WorkflowState WaitUntil/Execute API
|
|
51
|
+
Data attributes can also be read by getDataAttributes API by external applications using Client
|
|
52
|
+
Search attributes can be read/upsert in WorkflowState WaitUntil/Execute API
|
|
53
|
+
Search attributes can also be read by GetSearchAttributes Client API by external applications.
|
|
54
|
+
External applications can also use "SearchWorkflow" API to find workflows by SQL-like query
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A persistence schema. Default to empty.
|
|
58
|
+
"""
|
|
59
|
+
return PersistenceSchema()
|
|
60
|
+
|
|
61
|
+
def get_persistence_options(self) -> PersistenceOptions:
|
|
62
|
+
return PersistenceOptions.get_default()
|
|
63
|
+
|
|
64
|
+
def get_communication_schema(self) -> CommunicationSchema:
|
|
65
|
+
"""
|
|
66
|
+
GetCommunicationSchema defines all the communication methods for this workflow, this includes
|
|
67
|
+
1. Signal channel
|
|
68
|
+
2. Interstate channel
|
|
69
|
+
Signal channel is for external applications to send signal to workflow execution.
|
|
70
|
+
ObjectWorkflow execution can listen on the signal in the WorkflowState WaitUntil API and receive in
|
|
71
|
+
the WorkflowState Execute API
|
|
72
|
+
InterStateChannel is for synchronization communications between WorkflowStates.
|
|
73
|
+
E.g. WorkflowStateA will continue after receiving a value from WorkflowStateB
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A communication schema. Default to empty.
|
|
77
|
+
"""
|
|
78
|
+
return CommunicationSchema()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_workflow_type(wf: ObjectWorkflow) -> str:
|
|
82
|
+
return wf.__class__.__name__
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_workflow_type_by_class(wf_class: type[ObjectWorkflow]) -> str:
|
|
86
|
+
return wf_class.__name__
|
dex/workflow_context.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from dex.dex_api.models.context import Context
|
|
19
|
+
from dex.utils.dex_typing import unset_to_none
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class WorkflowContext:
|
|
24
|
+
workflow_id: str
|
|
25
|
+
workflow_run_id: str
|
|
26
|
+
workflow_start_timestamp_seconds: int
|
|
27
|
+
state_execution_id: Optional[str] = None
|
|
28
|
+
first_attempt_timestamp_seconds: Optional[int] = None
|
|
29
|
+
attempt: Optional[int] = None
|
|
30
|
+
child_workflow_request_id: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _from_idl_context(idl_context: Context) -> WorkflowContext:
|
|
34
|
+
state_execution_id = unset_to_none(idl_context.state_execution_id)
|
|
35
|
+
|
|
36
|
+
return WorkflowContext(
|
|
37
|
+
workflow_id=idl_context.workflow_id,
|
|
38
|
+
workflow_run_id=idl_context.workflow_run_id,
|
|
39
|
+
workflow_start_timestamp_seconds=idl_context.workflow_started_timestamp,
|
|
40
|
+
state_execution_id=state_execution_id,
|
|
41
|
+
first_attempt_timestamp_seconds=unset_to_none(
|
|
42
|
+
idl_context.first_attempt_timestamp,
|
|
43
|
+
),
|
|
44
|
+
attempt=unset_to_none(idl_context.attempt),
|
|
45
|
+
child_workflow_request_id=(
|
|
46
|
+
idl_context.workflow_run_id + "-" + state_execution_id
|
|
47
|
+
if state_execution_id is not None
|
|
48
|
+
else None
|
|
49
|
+
),
|
|
50
|
+
)
|
dex/workflow_info.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from dex.dex_api.models.workflow_status import WorkflowStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class WorkflowInfo:
|
|
21
|
+
workflow_status: WorkflowStatus
|
dex/workflow_options.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from dex.dex_api.models import (
|
|
19
|
+
IDReusePolicy,
|
|
20
|
+
WorkflowRetryPolicy,
|
|
21
|
+
WorkflowAlreadyStartedOptions,
|
|
22
|
+
WorkflowConfig,
|
|
23
|
+
)
|
|
24
|
+
from dex.workflow_state import (
|
|
25
|
+
WorkflowState,
|
|
26
|
+
get_state_id_by_class,
|
|
27
|
+
get_state_execution_id,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class WorkflowOptions:
|
|
33
|
+
workflow_id_reuse_policy: Optional[IDReusePolicy] = None
|
|
34
|
+
workflow_cron_schedule: Optional[str] = None
|
|
35
|
+
workflow_start_delay_seconds: Optional[int] = None
|
|
36
|
+
workflow_retry_policy: Optional[WorkflowRetryPolicy] = None
|
|
37
|
+
workflow_already_started_options: Optional[WorkflowAlreadyStartedOptions] = None
|
|
38
|
+
workflow_config_override: Optional[WorkflowConfig] = None
|
|
39
|
+
initial_data_attributes: Optional[dict[str, Any]] = None
|
|
40
|
+
_wait_for_completion_state_ids: list[str] = field(default_factory=list)
|
|
41
|
+
_wait_for_completion_state_execution_ids: list[str] = field(default_factory=list)
|
|
42
|
+
initial_search_attributes: Optional[dict[str, Any]] = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def wait_for_completion_state_ids(self) -> Optional[list[str]]:
|
|
46
|
+
return self._wait_for_completion_state_ids
|
|
47
|
+
|
|
48
|
+
@wait_for_completion_state_ids.setter
|
|
49
|
+
def wait_for_completion_state_ids(self, *states: type[WorkflowState]):
|
|
50
|
+
state_ids: list[str] = []
|
|
51
|
+
for state in states:
|
|
52
|
+
state_ids.append(get_state_id_by_class(state))
|
|
53
|
+
self._wait_for_completion_state_ids = state_ids
|
|
54
|
+
|
|
55
|
+
def add_wait_for_completion_state_ids(self, *states: type[WorkflowState]):
|
|
56
|
+
for state in states:
|
|
57
|
+
self._wait_for_completion_state_ids.append(get_state_id_by_class(state))
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def wait_for_completion_state_execution_ids(self) -> Optional[list[str]]:
|
|
61
|
+
return self._wait_for_completion_state_execution_ids
|
|
62
|
+
|
|
63
|
+
@wait_for_completion_state_execution_ids.setter
|
|
64
|
+
def wait_for_completion_state_execution_ids(self, val):
|
|
65
|
+
try:
|
|
66
|
+
state, number = val
|
|
67
|
+
except ValueError:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Pass an iterable with two items: state: type[WorkflowState] and number: int"
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
state_id = get_state_execution_id(state, number)
|
|
73
|
+
self._wait_for_completion_state_execution_ids = state_id
|
|
74
|
+
|
|
75
|
+
def add_wait_for_completion_state_execution_id(
|
|
76
|
+
self, state: type[WorkflowState], number: int
|
|
77
|
+
):
|
|
78
|
+
state_id = get_state_execution_id(state, number)
|
|
79
|
+
self._wait_for_completion_state_execution_ids.append(state_id)
|