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/search_attributes.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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 typing import Union
|
|
16
|
+
|
|
17
|
+
from dex.errors import WorkflowDefinitionError
|
|
18
|
+
from dex.dex_api.models import SearchAttribute, SearchAttributeValueType
|
|
19
|
+
from dex.utils.dex_typing import unset_to_none
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SearchAttributes:
|
|
23
|
+
_key_to_type_map: dict[str, SearchAttributeValueType]
|
|
24
|
+
_int64_attribute_map: dict[str, Union[int, None]]
|
|
25
|
+
_upsert_to_server_int64_attribute_map: dict[str, Union[int, None]]
|
|
26
|
+
_string_attribute_map: dict[str, Union[str, None]]
|
|
27
|
+
_upsert_to_server_string_attribute_map: dict[str, Union[str, None]]
|
|
28
|
+
_double_attribute_map: dict[str, Union[float, None]]
|
|
29
|
+
_upsert_to_server_double_attribute_map: dict[str, Union[float, None]]
|
|
30
|
+
_bool_attribute_map: dict[str, Union[bool, None]]
|
|
31
|
+
_upsert_to_server_bool_attribute_map: dict[str, Union[bool, None]]
|
|
32
|
+
_string_array_attribute_map: dict[str, Union[list[str], None]]
|
|
33
|
+
_upsert_to_server_string_array_attribute_map: dict[str, Union[list[str], None]]
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
key_to_type_map: dict[str, SearchAttributeValueType],
|
|
38
|
+
search_attribute_map: Union[list[SearchAttribute], None],
|
|
39
|
+
):
|
|
40
|
+
self._key_to_type_map = key_to_type_map
|
|
41
|
+
self._int64_attribute_map = {}
|
|
42
|
+
self._upsert_to_server_int64_attribute_map = {}
|
|
43
|
+
self._string_attribute_map = {}
|
|
44
|
+
self._upsert_to_server_string_attribute_map = {}
|
|
45
|
+
self._double_attribute_map = {}
|
|
46
|
+
self._upsert_to_server_double_attribute_map = {}
|
|
47
|
+
self._bool_attribute_map = {}
|
|
48
|
+
self._upsert_to_server_bool_attribute_map = {}
|
|
49
|
+
self._string_array_attribute_map = {}
|
|
50
|
+
self._upsert_to_server_string_array_attribute_map = {}
|
|
51
|
+
|
|
52
|
+
if search_attribute_map is not None:
|
|
53
|
+
for attribute in search_attribute_map:
|
|
54
|
+
key = unset_to_none(attribute.key)
|
|
55
|
+
if key is None:
|
|
56
|
+
raise ValueError("empty search attribute value type is not allowed")
|
|
57
|
+
value_type = key_to_type_map[key]
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
value_type == SearchAttributeValueType.KEYWORD
|
|
61
|
+
or value_type == SearchAttributeValueType.DATETIME
|
|
62
|
+
or value_type == SearchAttributeValueType.TEXT
|
|
63
|
+
):
|
|
64
|
+
self._string_attribute_map[key] = unset_to_none(
|
|
65
|
+
attribute.string_value
|
|
66
|
+
)
|
|
67
|
+
elif value_type == SearchAttributeValueType.INT:
|
|
68
|
+
self._int64_attribute_map[key] = unset_to_none(
|
|
69
|
+
attribute.integer_value
|
|
70
|
+
)
|
|
71
|
+
elif value_type == SearchAttributeValueType.DOUBLE:
|
|
72
|
+
self._double_attribute_map[key] = unset_to_none(
|
|
73
|
+
attribute.double_value
|
|
74
|
+
)
|
|
75
|
+
elif value_type == SearchAttributeValueType.BOOL:
|
|
76
|
+
self._bool_attribute_map[key] = unset_to_none(attribute.bool_value)
|
|
77
|
+
elif value_type == SearchAttributeValueType.KEYWORD_ARRAY:
|
|
78
|
+
self._string_array_attribute_map[key] = unset_to_none(
|
|
79
|
+
attribute.string_array_value
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"not supported search attribute value type, {value_type}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def get_search_attribute_int64(self, key: str) -> Union[int, None]:
|
|
87
|
+
return self._int64_attribute_map.get(key)
|
|
88
|
+
|
|
89
|
+
def set_search_attribute_int64(self, key: str, value: Union[int, None]):
|
|
90
|
+
if (
|
|
91
|
+
key not in self._key_to_type_map
|
|
92
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.INT
|
|
93
|
+
):
|
|
94
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as int64")
|
|
95
|
+
self._int64_attribute_map[key] = value
|
|
96
|
+
self._upsert_to_server_int64_attribute_map[key] = value
|
|
97
|
+
|
|
98
|
+
def get_search_attribute_double(self, key: str) -> Union[float, None]:
|
|
99
|
+
return self._double_attribute_map.get(key)
|
|
100
|
+
|
|
101
|
+
def set_search_attribute_double(self, key: str, value: Union[float, None]):
|
|
102
|
+
if (
|
|
103
|
+
key not in self._key_to_type_map
|
|
104
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.DOUBLE
|
|
105
|
+
):
|
|
106
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as double")
|
|
107
|
+
self._double_attribute_map[key] = value
|
|
108
|
+
self._upsert_to_server_double_attribute_map[key] = value
|
|
109
|
+
|
|
110
|
+
def get_search_attribute_boolean(self, key: str) -> Union[bool, None]:
|
|
111
|
+
return self._bool_attribute_map.get(key)
|
|
112
|
+
|
|
113
|
+
def set_search_attribute_boolean(self, key: str, value: Union[bool, None]):
|
|
114
|
+
if (
|
|
115
|
+
key not in self._key_to_type_map
|
|
116
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.BOOL
|
|
117
|
+
):
|
|
118
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as bool")
|
|
119
|
+
self._bool_attribute_map[key] = value
|
|
120
|
+
self._upsert_to_server_bool_attribute_map[key] = value
|
|
121
|
+
|
|
122
|
+
def get_search_attribute_keyword(self, key: str) -> Union[str, None]:
|
|
123
|
+
return self._string_attribute_map.get(key)
|
|
124
|
+
|
|
125
|
+
def set_search_attribute_keyword(self, key: str, value: Union[str, None]):
|
|
126
|
+
if (
|
|
127
|
+
key not in self._key_to_type_map
|
|
128
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD
|
|
129
|
+
):
|
|
130
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as keyword")
|
|
131
|
+
self._string_attribute_map[key] = value
|
|
132
|
+
self._upsert_to_server_string_attribute_map[key] = value
|
|
133
|
+
|
|
134
|
+
def get_search_attribute_text(self, key: str) -> Union[str, None]:
|
|
135
|
+
return self._string_attribute_map.get(key)
|
|
136
|
+
|
|
137
|
+
def set_search_attribute_text(self, key: str, value: Union[str, None]):
|
|
138
|
+
if (
|
|
139
|
+
key not in self._key_to_type_map
|
|
140
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.TEXT
|
|
141
|
+
):
|
|
142
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as text")
|
|
143
|
+
self._string_attribute_map[key] = value
|
|
144
|
+
self._upsert_to_server_string_attribute_map[key] = value
|
|
145
|
+
|
|
146
|
+
def get_search_attribute_datetime(self, key: str) -> Union[str, None]:
|
|
147
|
+
return self._string_attribute_map.get(key)
|
|
148
|
+
|
|
149
|
+
def set_search_attribute_datetime(self, key: str, value: Union[str, None]):
|
|
150
|
+
if (
|
|
151
|
+
key not in self._key_to_type_map
|
|
152
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.DATETIME
|
|
153
|
+
):
|
|
154
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as datetime")
|
|
155
|
+
self._string_attribute_map[key] = value
|
|
156
|
+
self._upsert_to_server_string_attribute_map[key] = value
|
|
157
|
+
|
|
158
|
+
def get_search_attribute_keyword_array(self, key: str) -> Union[list[str], None]:
|
|
159
|
+
return self._string_array_attribute_map.get(key)
|
|
160
|
+
|
|
161
|
+
def set_search_attribute_keyword_array(
|
|
162
|
+
self, key: str, value: Union[list[str], None]
|
|
163
|
+
):
|
|
164
|
+
if (
|
|
165
|
+
key not in self._key_to_type_map
|
|
166
|
+
or self._key_to_type_map[key] != SearchAttributeValueType.KEYWORD_ARRAY
|
|
167
|
+
):
|
|
168
|
+
raise WorkflowDefinitionError(f"key {key} is not defined as keyword array")
|
|
169
|
+
self._string_array_attribute_map[key] = value
|
|
170
|
+
self._upsert_to_server_string_array_attribute_map[key] = value
|
|
171
|
+
|
|
172
|
+
def get_upsert_to_server_int64_attribute_map(self) -> dict[str, Union[int, None]]:
|
|
173
|
+
return self._upsert_to_server_int64_attribute_map
|
|
174
|
+
|
|
175
|
+
def get_upsert_to_server_string_attribute_map(self) -> dict[str, Union[str, None]]:
|
|
176
|
+
return self._upsert_to_server_string_attribute_map
|
|
177
|
+
|
|
178
|
+
def get_upsert_to_server_string_array_attribute_map(
|
|
179
|
+
self,
|
|
180
|
+
) -> dict[str, Union[list[str], None]]:
|
|
181
|
+
return self._upsert_to_server_string_array_attribute_map
|
|
182
|
+
|
|
183
|
+
def get_upsert_to_server_bool_attribute_map(self) -> dict[str, Union[bool, None]]:
|
|
184
|
+
return self._upsert_to_server_bool_attribute_map
|
|
185
|
+
|
|
186
|
+
def get_upsert_to_server_double_attribute_map(
|
|
187
|
+
self,
|
|
188
|
+
) -> dict[str, Union[float, None]]:
|
|
189
|
+
return self._upsert_to_server_double_attribute_map
|
dex/state_decision.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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 __future__ import annotations # <-- Additional import.
|
|
16
|
+
|
|
17
|
+
import typing
|
|
18
|
+
|
|
19
|
+
from dex.dex_api.models import WorkflowConditionalClose, WorkflowConditionalCloseType
|
|
20
|
+
from dex.workflow_state_options import WorkflowStateOptions
|
|
21
|
+
|
|
22
|
+
if typing.TYPE_CHECKING:
|
|
23
|
+
from dex.registry import Registry
|
|
24
|
+
from dex.workflow_state import WorkflowState
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any, List, Union, Optional
|
|
28
|
+
|
|
29
|
+
from dex.dex_api.models.state_decision import StateDecision as IdlStateDecision
|
|
30
|
+
|
|
31
|
+
from dex.object_encoder import ObjectEncoder
|
|
32
|
+
|
|
33
|
+
from dex.state_movement import StateMovement, _to_idl_state_movement
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class InternalConditionalClose:
|
|
38
|
+
conditional_close_type: WorkflowConditionalCloseType
|
|
39
|
+
|
|
40
|
+
channel_name: str
|
|
41
|
+
|
|
42
|
+
close_input: Any = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class StateDecision:
|
|
47
|
+
next_states: List[StateMovement]
|
|
48
|
+
|
|
49
|
+
conditional_close: typing.Optional[InternalConditionalClose] = None
|
|
50
|
+
|
|
51
|
+
dead_end: typing.ClassVar[StateDecision]
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def graceful_complete_workflow(cls, output: Any = None) -> StateDecision:
|
|
55
|
+
return StateDecision([StateMovement.graceful_complete_workflow(output)])
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def force_complete_workflow(cls, output: Any = None) -> StateDecision:
|
|
59
|
+
return StateDecision([StateMovement.force_complete_workflow(output)])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def force_fail_workflow(cls, output: Any = None) -> StateDecision:
|
|
63
|
+
return StateDecision([StateMovement.force_fail_workflow(output)])
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def single_next_state(
|
|
67
|
+
cls,
|
|
68
|
+
state: Union[str, type[WorkflowState]],
|
|
69
|
+
state_input: Any = None,
|
|
70
|
+
state_options_override: Optional[WorkflowStateOptions] = None,
|
|
71
|
+
wait_for_key: Optional[str] = None,
|
|
72
|
+
) -> StateDecision:
|
|
73
|
+
return StateDecision(
|
|
74
|
+
[
|
|
75
|
+
StateMovement.create(
|
|
76
|
+
state, state_input, state_options_override, wait_for_key
|
|
77
|
+
)
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def multi_next_states(
|
|
83
|
+
cls, *next_states: Union[type[WorkflowState], StateMovement]
|
|
84
|
+
) -> StateDecision:
|
|
85
|
+
next_list = [
|
|
86
|
+
n if isinstance(n, StateMovement) else StateMovement.create(n)
|
|
87
|
+
for n in next_states
|
|
88
|
+
]
|
|
89
|
+
return StateDecision(next_list)
|
|
90
|
+
|
|
91
|
+
# Atomically force complete the workflow if internal channel is empty, otherwise trigger the state movements from the current thread
|
|
92
|
+
# This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
|
|
93
|
+
# Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
|
|
94
|
+
# execution of state APIs.
|
|
95
|
+
#
|
|
96
|
+
# Note that it's only for internal messages published from RPCs.
|
|
97
|
+
# It doesn't cover the cases that internal messages are published from other State APIs.
|
|
98
|
+
# If you do want to use other State APIs to publish messages to the channel at the same time, you can use persistence locking to
|
|
99
|
+
# ensure only the State APIs are not executed in parallel.
|
|
100
|
+
@classmethod
|
|
101
|
+
def force_complete_if_internal_channel_empty_or_else(
|
|
102
|
+
cls,
|
|
103
|
+
internal_channel_name: str,
|
|
104
|
+
workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
|
|
105
|
+
or_else_state: Union[
|
|
106
|
+
str, type[WorkflowState]
|
|
107
|
+
] = "", # required not empty -- if channel is NOT empty, go to this state with the state input
|
|
108
|
+
state_input: Any = None,
|
|
109
|
+
) -> StateDecision:
|
|
110
|
+
return StateDecision(
|
|
111
|
+
[StateMovement.create(or_else_state, state_input)],
|
|
112
|
+
InternalConditionalClose(
|
|
113
|
+
WorkflowConditionalCloseType.FORCE_COMPLETE_ON_INTERNAL_CHANNEL_EMPTY,
|
|
114
|
+
internal_channel_name,
|
|
115
|
+
workflow_complete_output,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Atomically force complete the workflow if signal channel is empty, otherwise trigger the state movements from the current thread
|
|
120
|
+
# This is to ensure all the messages in the channel are processed before completing the workflow, otherwise messages may be lost.
|
|
121
|
+
# Without this atomic API, if just checking the channel emptiness in the State WaitUntil, a workflow may receive new messages during the
|
|
122
|
+
# execution of state APIs.
|
|
123
|
+
@classmethod
|
|
124
|
+
def force_complete_if_signal_channel_empty_or_else(
|
|
125
|
+
cls,
|
|
126
|
+
signal_channel_name: str,
|
|
127
|
+
workflow_complete_output: Any = None, # if channel is empty, complete the workflow with the output
|
|
128
|
+
or_else_state: Union[
|
|
129
|
+
str, type[WorkflowState]
|
|
130
|
+
] = "", # required not empty-- if channel is NOT empty, go to this state with the state input
|
|
131
|
+
state_input: Any = None,
|
|
132
|
+
) -> StateDecision:
|
|
133
|
+
return StateDecision(
|
|
134
|
+
[StateMovement.create(or_else_state, state_input)],
|
|
135
|
+
InternalConditionalClose(
|
|
136
|
+
WorkflowConditionalCloseType.FORCE_COMPLETE_ON_SIGNAL_CHANNEL_EMPTY,
|
|
137
|
+
signal_channel_name,
|
|
138
|
+
workflow_complete_output,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
StateDecision.dead_end = StateDecision([StateMovement.dead_end])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _to_idl_state_decision(
|
|
147
|
+
decision: StateDecision, wf_type: str, registry: Registry, encoder: ObjectEncoder
|
|
148
|
+
) -> IdlStateDecision:
|
|
149
|
+
idl_decision = IdlStateDecision()
|
|
150
|
+
if len(decision.next_states) > 0:
|
|
151
|
+
idl_decision.next_states = [
|
|
152
|
+
_to_idl_state_movement(movement, wf_type, registry, encoder)
|
|
153
|
+
for movement in decision.next_states
|
|
154
|
+
]
|
|
155
|
+
if decision.conditional_close is not None:
|
|
156
|
+
idl_decision.conditional_close = WorkflowConditionalClose(
|
|
157
|
+
conditional_close_type=decision.conditional_close.conditional_close_type,
|
|
158
|
+
channel_name=decision.conditional_close.channel_name,
|
|
159
|
+
close_input=encoder.encode(decision.conditional_close.close_input),
|
|
160
|
+
)
|
|
161
|
+
return idl_decision
|
|
@@ -0,0 +1,71 @@
|
|
|
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 typing import Any, List, Tuple, Union
|
|
16
|
+
|
|
17
|
+
from dex.errors import WorkflowDefinitionError
|
|
18
|
+
from dex.dex_api.models import EncodedObject, KeyValue
|
|
19
|
+
from dex.dex_api.types import Unset
|
|
20
|
+
from dex.object_encoder import ObjectEncoder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StateExecutionLocals:
|
|
24
|
+
_record_events: dict[str, Union[EncodedObject, Unset]]
|
|
25
|
+
_attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]]
|
|
26
|
+
_upsert_attributes_to_return_to_server: dict[str, Union[EncodedObject, Unset]]
|
|
27
|
+
_object_encoder: ObjectEncoder
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
attribute_name_to_encoded_object_map: dict[str, Union[EncodedObject, Unset]],
|
|
32
|
+
object_encoder: ObjectEncoder,
|
|
33
|
+
):
|
|
34
|
+
self._object_encoder = object_encoder
|
|
35
|
+
self._attribute_name_to_encoded_object_map = (
|
|
36
|
+
attribute_name_to_encoded_object_map
|
|
37
|
+
)
|
|
38
|
+
self._upsert_attributes_to_return_to_server = {}
|
|
39
|
+
self._record_events = {}
|
|
40
|
+
|
|
41
|
+
def set_state_execution_local(self, key: str, value: Any):
|
|
42
|
+
encoded_data = self._object_encoder.encode(value)
|
|
43
|
+
self._attribute_name_to_encoded_object_map[key] = encoded_data
|
|
44
|
+
self._upsert_attributes_to_return_to_server[key] = encoded_data
|
|
45
|
+
|
|
46
|
+
def get_state_execution_local(self, key: str) -> Any:
|
|
47
|
+
encoded_object = self._attribute_name_to_encoded_object_map.get(key)
|
|
48
|
+
if encoded_object is None:
|
|
49
|
+
return None
|
|
50
|
+
return self._object_encoder.decode(encoded_object)
|
|
51
|
+
|
|
52
|
+
def record_event(self, key: str, *event_data: Tuple[Any, ...]):
|
|
53
|
+
if key in self._record_events:
|
|
54
|
+
raise WorkflowDefinitionError("Cannot record the same event more than once")
|
|
55
|
+
|
|
56
|
+
if event_data is not None and len(event_data) == 1:
|
|
57
|
+
self._record_events[key] = self._object_encoder.encode(event_data[0])
|
|
58
|
+
|
|
59
|
+
self._record_events[key] = self._object_encoder.encode(event_data)
|
|
60
|
+
|
|
61
|
+
def get_upsert_state_execution_local_attributes(self) -> List[KeyValue]:
|
|
62
|
+
return [
|
|
63
|
+
KeyValue(item_key, item_value)
|
|
64
|
+
for item_key, item_value in self._upsert_attributes_to_return_to_server.items()
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def get_record_events(self) -> List[KeyValue]:
|
|
68
|
+
return [
|
|
69
|
+
KeyValue(item_key, item_value)
|
|
70
|
+
for item_key, item_value in self._record_events.items()
|
|
71
|
+
]
|
dex/state_movement.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
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 __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import typing
|
|
18
|
+
from typing import Union, Optional
|
|
19
|
+
|
|
20
|
+
from dex.errors import WorkflowDefinitionError
|
|
21
|
+
|
|
22
|
+
if typing.TYPE_CHECKING:
|
|
23
|
+
from dex.workflow_state import (
|
|
24
|
+
WorkflowState,
|
|
25
|
+
)
|
|
26
|
+
from dex.registry import Registry
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from dex.dex_api.models.state_movement import StateMovement as IdlStateMovement
|
|
33
|
+
|
|
34
|
+
from dex.object_encoder import ObjectEncoder
|
|
35
|
+
|
|
36
|
+
from dex.workflow_state_options import _to_idl_state_options, WorkflowStateOptions
|
|
37
|
+
|
|
38
|
+
reserved_state_id_prefix = "_SYS_"
|
|
39
|
+
|
|
40
|
+
graceful_completing_sys_state_id = (
|
|
41
|
+
reserved_state_id_prefix + "GRACEFUL_COMPLETING_WORKFLOW"
|
|
42
|
+
)
|
|
43
|
+
force_completing_sys_state_id = reserved_state_id_prefix + "FORCE_COMPLETING_WORKFLOW"
|
|
44
|
+
force_failing_sys_state_id = reserved_state_id_prefix + "FORCE_FAILING_WORKFLOW"
|
|
45
|
+
dead_end_sys_state_id = reserved_state_id_prefix + "DEAD_END"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class StateMovement:
|
|
50
|
+
state_id: str
|
|
51
|
+
state_input: Any = None
|
|
52
|
+
state_options_override: Optional[WorkflowStateOptions] = None
|
|
53
|
+
wait_for_key: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
dead_end: typing.ClassVar[StateMovement]
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def graceful_complete_workflow(cls, output: Any = None) -> StateMovement:
|
|
59
|
+
return StateMovement(graceful_completing_sys_state_id, output)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def force_complete_workflow(cls, output: Any = None) -> StateMovement:
|
|
63
|
+
return StateMovement(force_completing_sys_state_id, output)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def force_fail_workflow(cls, output: Any = None) -> StateMovement:
|
|
67
|
+
return StateMovement(force_failing_sys_state_id, output)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def create(
|
|
71
|
+
cls,
|
|
72
|
+
state: Union[str, type[WorkflowState]],
|
|
73
|
+
state_input: Any = None,
|
|
74
|
+
state_options_override: Optional[WorkflowStateOptions] = None,
|
|
75
|
+
wait_for_key: Optional[str] = None,
|
|
76
|
+
) -> StateMovement:
|
|
77
|
+
if isinstance(state, str):
|
|
78
|
+
state_id = state
|
|
79
|
+
else:
|
|
80
|
+
from dex.workflow_state import (
|
|
81
|
+
get_state_id_by_class,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
state_id = get_state_id_by_class(state)
|
|
85
|
+
if state_id.startswith(reserved_state_id_prefix):
|
|
86
|
+
raise WorkflowDefinitionError("cannot use reserved stateId")
|
|
87
|
+
return StateMovement(
|
|
88
|
+
state_id, state_input, state_options_override, wait_for_key
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
StateMovement.dead_end = StateMovement(dead_end_sys_state_id)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _to_idl_state_movement(
|
|
96
|
+
movement: StateMovement, wf_type: str, registry: Registry, encoder: ObjectEncoder
|
|
97
|
+
) -> IdlStateMovement:
|
|
98
|
+
idl_movement = IdlStateMovement(
|
|
99
|
+
state_id=movement.state_id, state_input=encoder.encode(movement.state_input)
|
|
100
|
+
)
|
|
101
|
+
if not movement.state_id.startswith(reserved_state_id_prefix):
|
|
102
|
+
state = registry.get_workflow_state_with_check(wf_type, movement.state_id)
|
|
103
|
+
|
|
104
|
+
from dex.workflow_state import (
|
|
105
|
+
should_skip_wait_until,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if movement.state_options_override is not None:
|
|
109
|
+
options = movement.state_options_override
|
|
110
|
+
else:
|
|
111
|
+
options = state.get_state_options()
|
|
112
|
+
|
|
113
|
+
idl_state_options = _to_idl_state_options(
|
|
114
|
+
should_skip_wait_until(state),
|
|
115
|
+
options,
|
|
116
|
+
registry.get_state_store(wf_type),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
idl_movement.state_options = idl_state_options
|
|
120
|
+
|
|
121
|
+
if movement.wait_for_key is not None:
|
|
122
|
+
idl_movement.wait_for_key = movement.wait_for_key
|
|
123
|
+
return idl_movement
|
dex/state_schema.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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 List
|
|
17
|
+
|
|
18
|
+
from dex.workflow_state import WorkflowState
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class StateDef:
|
|
23
|
+
state: WorkflowState
|
|
24
|
+
can_start_workflow: bool
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def starting_state(cls, state: WorkflowState):
|
|
28
|
+
return StateDef(state, True)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def non_starting_state(cls, state: WorkflowState):
|
|
32
|
+
return StateDef(state, False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class StateSchema:
|
|
37
|
+
states: List[StateDef] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
# TODO: it's super weird that we can't use type hint here " ->StateSchema" for return
|
|
40
|
+
# But the pattern works for state_movement.py
|
|
41
|
+
@classmethod
|
|
42
|
+
def with_starting_state(
|
|
43
|
+
cls, starting_state: WorkflowState, *non_starting_states: WorkflowState
|
|
44
|
+
):
|
|
45
|
+
return StateSchema(
|
|
46
|
+
[StateDef.starting_state(starting_state)]
|
|
47
|
+
+ [StateDef.non_starting_state(s) for s in non_starting_states]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def no_starting_state(cls, *non_starting_states: WorkflowState):
|
|
52
|
+
return StateSchema(
|
|
53
|
+
[StateDef.non_starting_state(s) for s in non_starting_states]
|
|
54
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
|
|
17
|
+
from dex.dex_api.models import WorkflowStopType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StopWorkflowOptions:
|
|
22
|
+
workflow_stop_type: WorkflowStopType
|
|
23
|
+
reason: str
|