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