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.
Files changed (93) hide show
  1. dex/__init__.py +14 -0
  2. dex/client.py +568 -0
  3. dex/client_options.py +34 -0
  4. dex/command_request.py +130 -0
  5. dex/command_results.py +116 -0
  6. dex/communication.py +141 -0
  7. dex/communication_schema.py +68 -0
  8. dex/data_attributes.py +75 -0
  9. dex/dexpb/__init__.py +1 -0
  10. dex/dexpb/dex_pb2.py +305 -0
  11. dex/dexpb/dex_pb2.pyi +1273 -0
  12. dex/dexpb/dex_pb2_grpc.py +1126 -0
  13. dex/errors.py +132 -0
  14. dex/object_encoder.py +818 -0
  15. dex/persistence.py +94 -0
  16. dex/persistence_options.py +24 -0
  17. dex/persistence_schema.py +58 -0
  18. dex/py.typed +1 -0
  19. dex/registry.py +209 -0
  20. dex/reset_workflow_type_and_options.py +77 -0
  21. dex/rpc.py +100 -0
  22. dex/search_attributes.py +189 -0
  23. dex/state_decision.py +161 -0
  24. dex/state_execution_locals.py +71 -0
  25. dex/state_movement.py +123 -0
  26. dex/state_schema.py +54 -0
  27. dex/stop_workflow_options.py +23 -0
  28. dex/tests/__init__.py +84 -0
  29. dex/tests/dex-service-env/.env +7 -0
  30. dex/tests/dex-service-env/docker-compose-init.sh +44 -0
  31. dex/tests/dex-service-env/docker-compose.yml +87 -0
  32. dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
  33. dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +6 -0
  34. dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -0
  35. dex/tests/test_abnormal_exit_workflow.py +48 -0
  36. dex/tests/test_basic_workflow.py +75 -0
  37. dex/tests/test_conditional_complete.py +56 -0
  38. dex/tests/test_describe_workflow.py +45 -0
  39. dex/tests/test_empty_data_decodes_properly.py +79 -0
  40. dex/tests/test_internal_channel.py +33 -0
  41. dex/tests/test_internal_channel_with_no_prefix_channel.py +46 -0
  42. dex/tests/test_persistence_data_attributes.py +67 -0
  43. dex/tests/test_persistence_search_attributes.py +132 -0
  44. dex/tests/test_persistence_state_execution_locals.py +43 -0
  45. dex/tests/test_rpc.py +69 -0
  46. dex/tests/test_rpc_with_memo.py +200 -0
  47. dex/tests/test_rpc_with_memo_duplicate_java_tests.py +122 -0
  48. dex/tests/test_signal.py +56 -0
  49. dex/tests/test_skip_wait_until.py +85 -0
  50. dex/tests/test_state_failure_recovery.py +33 -0
  51. dex/tests/test_timer.py +40 -0
  52. dex/tests/test_wait_for_state_execution_completion.py +58 -0
  53. dex/tests/test_workflow_errors.py +92 -0
  54. dex/tests/test_workflow_state_options.py +123 -0
  55. dex/tests/test_workflow_state_options_override.py +49 -0
  56. dex/tests/worker_server.py +74 -0
  57. dex/tests/workflows/abnormal_exit_workflow.py +48 -0
  58. dex/tests/workflows/basic_workflow.py +69 -0
  59. dex/tests/workflows/conditional_complete_workflow.py +101 -0
  60. dex/tests/workflows/describe_workflow.py +52 -0
  61. dex/tests/workflows/empty_data_workflow.py +51 -0
  62. dex/tests/workflows/internal_channel_workflow.py +137 -0
  63. dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +108 -0
  64. dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +285 -0
  65. dex/tests/workflows/persistence_data_attributes_workflow.py +104 -0
  66. dex/tests/workflows/persistence_search_attributes_workflow.py +167 -0
  67. dex/tests/workflows/persistence_state_execution_local_workflow.py +69 -0
  68. dex/tests/workflows/recovery_workflow.py +90 -0
  69. dex/tests/workflows/rpc_memo_workflow.py +238 -0
  70. dex/tests/workflows/rpc_workflow.py +125 -0
  71. dex/tests/workflows/state_options_override_workflow.py +100 -0
  72. dex/tests/workflows/state_options_workflow.py +92 -0
  73. dex/tests/workflows/timer_workflow.py +52 -0
  74. dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +77 -0
  75. dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +78 -0
  76. dex/tests/workflows/wait_internal_channel_workflow.py +53 -0
  77. dex/tests/workflows/wait_signal_workflow.py +154 -0
  78. dex/type_store.py +105 -0
  79. dex/unregistered_client.py +595 -0
  80. dex/utils/__init__.py +14 -0
  81. dex/utils/dex_typing.py +31 -0
  82. dex/utils/persistence_utils.py +37 -0
  83. dex/worker_service.py +441 -0
  84. dex/workflow.py +86 -0
  85. dex/workflow_context.py +50 -0
  86. dex/workflow_info.py +21 -0
  87. dex/workflow_options.py +79 -0
  88. dex/workflow_state.py +134 -0
  89. dex/workflow_state_options.py +160 -0
  90. dex_python_sdk-0.0.1.dist-info/METADATA +140 -0
  91. dex_python_sdk-0.0.1.dist-info/RECORD +93 -0
  92. dex_python_sdk-0.0.1.dist-info/WHEEL +4 -0
  93. dex_python_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,77 @@
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 dex.command_request import CommandRequest, TimerCommand
16
+ from dex.command_results import CommandResults
17
+ from dex.communication import Communication
18
+ from dex.persistence import Persistence
19
+ from dex.state_decision import StateDecision
20
+ from dex.state_schema import StateSchema
21
+ from dex.workflow import ObjectWorkflow
22
+ from dex.workflow_context import WorkflowContext
23
+ from dex.workflow_state import T, WorkflowState
24
+
25
+
26
+ class WaitForStateWithStateExecutionIdState1(WorkflowState[None]):
27
+ def wait_until(
28
+ self,
29
+ ctx: WorkflowContext,
30
+ input: T,
31
+ persistence: Persistence,
32
+ communication: Communication,
33
+ ) -> CommandRequest:
34
+ return CommandRequest.for_all_command_completed(
35
+ TimerCommand.by_seconds(2),
36
+ )
37
+
38
+ def execute(
39
+ self,
40
+ ctx: WorkflowContext,
41
+ input: T,
42
+ command_results: CommandResults,
43
+ persistence: Persistence,
44
+ communication: Communication,
45
+ ) -> StateDecision:
46
+ return StateDecision.single_next_state(WaitForStateWithStateExecutionIdState2)
47
+
48
+
49
+ class WaitForStateWithStateExecutionIdState2(WorkflowState[None]):
50
+ def wait_until(
51
+ self,
52
+ ctx: WorkflowContext,
53
+ input: T,
54
+ persistence: Persistence,
55
+ communication: Communication,
56
+ ) -> CommandRequest:
57
+ return CommandRequest.for_all_command_completed(
58
+ TimerCommand.by_seconds(2),
59
+ )
60
+
61
+ def execute(
62
+ self,
63
+ ctx: WorkflowContext,
64
+ input: T,
65
+ command_results: CommandResults,
66
+ persistence: Persistence,
67
+ communication: Communication,
68
+ ) -> StateDecision:
69
+ return StateDecision.graceful_complete_workflow()
70
+
71
+
72
+ class WaitForStateWithStateExecutionIdWorkflow(ObjectWorkflow):
73
+ def get_workflow_states(self) -> StateSchema:
74
+ return StateSchema.with_starting_state(
75
+ WaitForStateWithStateExecutionIdState1(),
76
+ WaitForStateWithStateExecutionIdState2(),
77
+ )
@@ -0,0 +1,78 @@
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 dex.command_request import CommandRequest, TimerCommand
16
+ from dex.command_results import CommandResults
17
+ from dex.communication import Communication
18
+ from dex.persistence import Persistence
19
+ from dex.state_decision import StateDecision
20
+ from dex.state_schema import StateSchema
21
+ from dex.workflow import ObjectWorkflow
22
+ from dex.workflow_context import WorkflowContext
23
+ from dex.workflow_state import T, WorkflowState
24
+
25
+
26
+ class WaitForStateWithWaitForKeyState1(WorkflowState[None]):
27
+ def wait_until(
28
+ self,
29
+ ctx: WorkflowContext,
30
+ input: T,
31
+ persistence: Persistence,
32
+ communication: Communication,
33
+ ) -> CommandRequest:
34
+ return CommandRequest.for_all_command_completed(
35
+ TimerCommand.by_seconds(2),
36
+ )
37
+
38
+ def execute(
39
+ self,
40
+ ctx: WorkflowContext,
41
+ input: T,
42
+ command_results: CommandResults,
43
+ persistence: Persistence,
44
+ communication: Communication,
45
+ ) -> StateDecision:
46
+ return StateDecision.single_next_state(
47
+ WaitForStateWithWaitForKeyState2, None, None, "testKey"
48
+ )
49
+
50
+
51
+ class WaitForStateWithWaitForKeyState2(WorkflowState[None]):
52
+ def wait_until(
53
+ self,
54
+ ctx: WorkflowContext,
55
+ input: T,
56
+ persistence: Persistence,
57
+ communication: Communication,
58
+ ) -> CommandRequest:
59
+ return CommandRequest.for_all_command_completed(
60
+ TimerCommand.by_seconds(2),
61
+ )
62
+
63
+ def execute(
64
+ self,
65
+ ctx: WorkflowContext,
66
+ input: T,
67
+ command_results: CommandResults,
68
+ persistence: Persistence,
69
+ communication: Communication,
70
+ ) -> StateDecision:
71
+ return StateDecision.graceful_complete_workflow()
72
+
73
+
74
+ class WaitForStateWithWaitForKeyWorkflow(ObjectWorkflow):
75
+ def get_workflow_states(self) -> StateSchema:
76
+ return StateSchema.with_starting_state(
77
+ WaitForStateWithWaitForKeyState1(), WaitForStateWithWaitForKeyState2()
78
+ )
@@ -0,0 +1,53 @@
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 dex.command_request import CommandRequest, InternalChannelCommand
16
+ from dex.command_results import CommandResults
17
+ from dex.communication import Communication
18
+ from dex.persistence import Persistence
19
+ from dex.state_decision import StateDecision
20
+ from dex.state_schema import StateSchema
21
+ from dex.workflow import ObjectWorkflow
22
+ from dex.workflow_context import WorkflowContext
23
+ from dex.workflow_state import T, WorkflowState
24
+
25
+ test_channel_name = "test-name"
26
+
27
+
28
+ class WaitState(WorkflowState[None]):
29
+ def wait_until(
30
+ self,
31
+ ctx: WorkflowContext,
32
+ input: T,
33
+ persistence: Persistence,
34
+ communication: Communication,
35
+ ) -> CommandRequest:
36
+ return CommandRequest.for_all_command_completed(
37
+ InternalChannelCommand.by_name(test_channel_name)
38
+ )
39
+
40
+ def execute(
41
+ self,
42
+ ctx: WorkflowContext,
43
+ input: T,
44
+ command_results: CommandResults,
45
+ persistence: Persistence,
46
+ communication: Communication,
47
+ ) -> StateDecision:
48
+ return StateDecision.graceful_complete_workflow()
49
+
50
+
51
+ class WaitInternalChannelWorkflow(ObjectWorkflow):
52
+ def get_workflow_states(self) -> StateSchema:
53
+ return StateSchema.with_starting_state(WaitState())
@@ -0,0 +1,154 @@
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 dex.dex_api.models.timer_result import TimerStatus
16
+ from dex.command_request import (
17
+ CommandRequest,
18
+ SignalChannelCommand,
19
+ TimerCommand,
20
+ )
21
+ from dex.command_results import (
22
+ CommandResults,
23
+ SignalChannelCommandResult,
24
+ TimerCommandResult,
25
+ )
26
+ from dex.communication import Communication
27
+ from dex.communication_schema import CommunicationMethod, CommunicationSchema
28
+ from dex.dex_api.models import ChannelRequestStatus
29
+ from dex.persistence import Persistence
30
+ from dex.rpc import rpc
31
+ from dex.state_decision import StateDecision
32
+ from dex.state_schema import StateSchema
33
+ from dex.workflow import ObjectWorkflow
34
+ from dex.workflow_context import WorkflowContext
35
+ from dex.workflow_state import T, WorkflowState
36
+
37
+ test_channel_int = "test-int"
38
+ test_channel_none = "test-none"
39
+ test_channel_str = "test-str"
40
+ test_idle_channel_none = "test-idle"
41
+
42
+ test_channel1 = "test-channel1"
43
+ test_channel1_id = "test-channel1-id"
44
+ test_channel2 = "test-channel2"
45
+ test_channel2_id = "test-channel2-id"
46
+ test_timer_id = "test-timer-id"
47
+
48
+
49
+ class WaitState1(WorkflowState[None]):
50
+ def wait_until(
51
+ self,
52
+ ctx: WorkflowContext,
53
+ input: T,
54
+ persistence: Persistence,
55
+ communication: Communication,
56
+ ) -> CommandRequest:
57
+ return CommandRequest.for_all_command_completed(
58
+ SignalChannelCommand.by_name(test_channel_int),
59
+ SignalChannelCommand.by_name(test_channel_none),
60
+ SignalChannelCommand.by_name(test_channel_str),
61
+ )
62
+
63
+ def execute(
64
+ self,
65
+ ctx: WorkflowContext,
66
+ input: T,
67
+ command_results: CommandResults,
68
+ persistence: Persistence,
69
+ communication: Communication,
70
+ ) -> StateDecision:
71
+ assert len(command_results.signal_channel_commands) == 3
72
+ sig1 = command_results.signal_channel_commands[0]
73
+ sig2 = command_results.signal_channel_commands[1]
74
+ sig3 = command_results.signal_channel_commands[2]
75
+ assert sig1 == SignalChannelCommandResult(
76
+ test_channel_int, 123, ChannelRequestStatus.RECEIVED, ""
77
+ )
78
+ assert sig2 == SignalChannelCommandResult(
79
+ test_channel_none, None, ChannelRequestStatus.RECEIVED, ""
80
+ )
81
+ assert sig3 == SignalChannelCommandResult(
82
+ test_channel_str, "abc", ChannelRequestStatus.RECEIVED, ""
83
+ )
84
+ return StateDecision.single_next_state(WaitState2, sig3.value)
85
+
86
+
87
+ class WaitState2(WorkflowState[str]):
88
+ def wait_until(
89
+ self,
90
+ ctx: WorkflowContext,
91
+ input: str,
92
+ persistence: Persistence,
93
+ communication: Communication,
94
+ ) -> CommandRequest:
95
+ return CommandRequest.for_any_command_combination_completed(
96
+ [
97
+ [
98
+ test_channel1_id,
99
+ test_timer_id,
100
+ ]
101
+ ],
102
+ SignalChannelCommand.by_name(test_channel1, test_channel1_id),
103
+ SignalChannelCommand.by_name(test_channel2, test_channel2_id),
104
+ TimerCommand.by_seconds(1, test_timer_id),
105
+ )
106
+
107
+ def execute(
108
+ self,
109
+ ctx: WorkflowContext,
110
+ input: str,
111
+ command_results: CommandResults,
112
+ persistence: Persistence,
113
+ communication: Communication,
114
+ ) -> StateDecision:
115
+ assert len(command_results.signal_channel_commands) == 2
116
+ assert (
117
+ len(
118
+ [
119
+ r
120
+ for r in command_results.signal_channel_commands
121
+ if r.status == ChannelRequestStatus.RECEIVED
122
+ ]
123
+ )
124
+ == 1
125
+ )
126
+ sig1 = command_results.signal_channel_commands[0]
127
+ tim1 = command_results.timer_commands[0]
128
+ assert sig1 == SignalChannelCommandResult(
129
+ test_channel1, None, ChannelRequestStatus.RECEIVED, test_channel1_id
130
+ )
131
+ assert tim1 == TimerCommandResult(
132
+ TimerStatus.FIRED,
133
+ test_timer_id,
134
+ )
135
+ return StateDecision.graceful_complete_workflow(input)
136
+
137
+
138
+ class WaitSignalWorkflow(ObjectWorkflow):
139
+ def get_communication_schema(self) -> CommunicationSchema:
140
+ return CommunicationSchema.create(
141
+ CommunicationMethod.signal_channel_def(test_channel_int, int),
142
+ CommunicationMethod.signal_channel_def(test_channel_none, None),
143
+ CommunicationMethod.signal_channel_def(test_channel_str, str),
144
+ CommunicationMethod.signal_channel_def(test_idle_channel_none, None),
145
+ CommunicationMethod.signal_channel_def(test_channel1, None),
146
+ CommunicationMethod.signal_channel_def(test_channel2, None),
147
+ )
148
+
149
+ def get_workflow_states(self) -> StateSchema:
150
+ return StateSchema.with_starting_state(WaitState1(), WaitState2())
151
+
152
+ @rpc()
153
+ def get_idle_signal_channel_size(self, com: Communication):
154
+ return com.get_signal_channel_size(test_idle_channel_none)
dex/type_store.py ADDED
@@ -0,0 +1,105 @@
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 Optional
16
+ from enum import Enum
17
+
18
+ from dex.communication_schema import CommunicationMethod
19
+ from dex.errors import WorkflowDefinitionError, NotRegisteredError
20
+ from dex.persistence_schema import PersistenceField, PersistenceFieldType
21
+
22
+
23
+ class Type(Enum):
24
+ INTERNAL_CHANNEL = 1
25
+ DATA_ATTRIBUTE = 2
26
+ # TODO: extend to other types
27
+ # SIGNAL_CHANNEL = 3
28
+
29
+
30
+ class TypeStore:
31
+ _class_type: Type
32
+ _name_to_type_store: dict[str, Optional[type]]
33
+ _prefix_to_type_store: dict[str, Optional[type]]
34
+
35
+ def __init__(self, class_type: Type):
36
+ self._class_type = class_type
37
+ self._name_to_type_store = dict()
38
+ self._prefix_to_type_store = dict()
39
+
40
+ def is_valid_name_or_prefix(self, name: str) -> bool:
41
+ return self._validate_name(name)
42
+
43
+ def get_type(self, name: str) -> type:
44
+ is_registered = self._validate_name(name)
45
+
46
+ if not is_registered:
47
+ raise NotRegisteredError(f"{self._class_type} not registered: {name}")
48
+
49
+ t = self._do_get_type(name)
50
+ if t is None:
51
+ raise NotRegisteredError(f"{self._class_type} not registered: {name}")
52
+
53
+ return t
54
+
55
+ def add_internal_channel_def(self, obj: CommunicationMethod):
56
+ if self._class_type != Type.INTERNAL_CHANNEL:
57
+ raise ValueError(
58
+ f"Cannot add internal channel definition to {self._class_type}"
59
+ )
60
+ self._do_add_to_store(obj.is_prefix, obj.name, obj.value_type)
61
+
62
+ def add_data_attribute_def(self, obj: PersistenceField):
63
+ if self._class_type != Type.DATA_ATTRIBUTE:
64
+ raise ValueError(
65
+ f"Cannot add internal channel definition to {self._class_type}"
66
+ )
67
+ self._do_add_to_store(
68
+ obj.field_type == PersistenceFieldType.DataAttributePrefix,
69
+ obj.key,
70
+ obj.value_type,
71
+ )
72
+
73
+ def _validate_name(self, name: str) -> bool:
74
+ if name in self._name_to_type_store:
75
+ return True
76
+
77
+ for prefix in self._prefix_to_type_store.keys():
78
+ if name.startswith(prefix):
79
+ return True
80
+
81
+ return False
82
+
83
+ def _do_get_type(self, name: str) -> Optional[type]:
84
+ if name in self._name_to_type_store:
85
+ t = self._name_to_type_store[name]
86
+ return t if t is not None else type(None)
87
+
88
+ for prefix, t in self._prefix_to_type_store.items():
89
+ if name.startswith(prefix):
90
+ return t if t is not None else type(None)
91
+
92
+ return None
93
+
94
+ def _do_add_to_store(self, is_prefix: bool, name: str, t: Optional[type]):
95
+ if is_prefix:
96
+ store = self._prefix_to_type_store
97
+ else:
98
+ store = self._name_to_type_store
99
+
100
+ if name in store:
101
+ raise WorkflowDefinitionError(
102
+ f"{self._class_type} name/prefix {name} already exists"
103
+ )
104
+
105
+ store[name] = t