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.
- dex/__init__.py +146 -0
- dex/_grpc_errors.py +73 -0
- dex/_invocation_context.py +269 -0
- dex/_native.pyd +0 -0
- dex/_native.pyi +20 -0
- dex/_utils.py +18 -0
- dex/_value_hydrator.py +223 -0
- dex/_value_mapper.py +174 -0
- dex/_worker_dispatcher.py +451 -0
- dex/_worker_service.py +60 -0
- dex/attribute.py +87 -0
- dex/blob_cache.py +73 -0
- dex/channel.py +165 -0
- dex/client.py +715 -0
- dex/client_options.py +19 -0
- dex/codec.py +286 -0
- dex/command_request.py +120 -0
- dex/command_results.py +107 -0
- dex/communication.py +136 -0
- dex/communication_schema.py +54 -0
- dex/condition.py +74 -0
- dex/context.py +86 -0
- dex/data_attributes.py +70 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +381 -0
- dex/dexpb/dex_pb2.pyi +1734 -0
- dex/dexpb/dex_pb2_grpc.py +1298 -0
- dex/errors.py +109 -0
- dex/flow.py +456 -0
- dex/flow_config.py +29 -0
- dex/flow_info.py +51 -0
- dex/flow_options.py +122 -0
- dex/object_encoder.py +799 -0
- dex/persistence.py +89 -0
- dex/persistence_options.py +12 -0
- dex/persistence_schema.py +51 -0
- dex/py.typed +1 -0
- dex/registry.py +204 -0
- dex/reset_workflow_type_and_options.py +67 -0
- dex/rpc.py +93 -0
- dex/runtime_errors.py +81 -0
- dex/search_attributes.py +184 -0
- dex/state_decision.py +153 -0
- dex/state_execution_locals.py +66 -0
- dex/state_movement.py +115 -0
- dex/state_schema.py +48 -0
- dex/step.py +194 -0
- dex/step_execution.py +42 -0
- dex/stop_workflow_options.py +18 -0
- dex/tests/__init__.py +80 -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 +97 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
- dex/tests/test_abnormal_exit_workflow.py +43 -0
- dex/tests/test_basic_workflow.py +70 -0
- dex/tests/test_conditional_complete.py +50 -0
- dex/tests/test_describe_workflow.py +40 -0
- dex/tests/test_empty_data_decodes_properly.py +74 -0
- dex/tests/test_internal_channel.py +28 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
- dex/tests/test_persistence_data_attributes.py +62 -0
- dex/tests/test_persistence_search_attributes.py +127 -0
- dex/tests/test_persistence_state_execution_locals.py +38 -0
- dex/tests/test_rpc.py +64 -0
- dex/tests/test_rpc_with_memo.py +195 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
- dex/tests/test_signal.py +51 -0
- dex/tests/test_skip_wait_until.py +76 -0
- dex/tests/test_state_failure_recovery.py +28 -0
- dex/tests/test_timer.py +35 -0
- dex/tests/test_wait_for_state_execution_completion.py +53 -0
- dex/tests/test_workflow_errors.py +87 -0
- dex/tests/test_workflow_state_options.py +118 -0
- dex/tests/test_workflow_state_options_override.py +44 -0
- dex/tests/worker_server.py +64 -0
- dex/tests/workflows/abnormal_exit_workflow.py +42 -0
- dex/tests/workflows/basic_workflow.py +62 -0
- dex/tests/workflows/conditional_complete_workflow.py +95 -0
- dex/tests/workflows/describe_workflow.py +46 -0
- dex/tests/workflows/empty_data_workflow.py +45 -0
- dex/tests/workflows/internal_channel_workflow.py +129 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
- dex/tests/workflows/recovery_workflow.py +82 -0
- dex/tests/workflows/rpc_memo_workflow.py +231 -0
- dex/tests/workflows/rpc_workflow.py +117 -0
- dex/tests/workflows/state_options_override_workflow.py +93 -0
- dex/tests/workflows/state_options_workflow.py +84 -0
- dex/tests/workflows/timer_workflow.py +46 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
- dex/tests/workflows/wait_signal_workflow.py +147 -0
- dex/timer.py +21 -0
- dex/type_store.py +99 -0
- dex/unregistered_client.py +585 -0
- dex/utils/__init__.py +3 -0
- dex/utils/dex_typing.py +25 -0
- dex/utils/persistence_utils.py +32 -0
- dex/wait.py +49 -0
- dex/worker.py +121 -0
- dex/worker_options.py +22 -0
- dex/worker_service.py +432 -0
- dex/workflow.py +79 -0
- dex/workflow_context.py +44 -0
- dex/workflow_info.py +16 -0
- dex/workflow_options.py +74 -0
- dex/workflow_state.py +123 -0
- dex/workflow_state_options.py +154 -0
- dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
- dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
- dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
- 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__
|
dex/workflow_context.py
ADDED
|
@@ -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
|
+
)
|