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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import unittest
|
|
16
|
+
|
|
17
|
+
from dex.command_request import CommandRequest
|
|
18
|
+
from dex.command_results import CommandResults
|
|
19
|
+
from dex.communication import Communication
|
|
20
|
+
from dex.persistence import Persistence
|
|
21
|
+
from dex.state_decision import StateDecision
|
|
22
|
+
from dex.workflow_context import WorkflowContext
|
|
23
|
+
from dex.workflow_state import T, WorkflowState, should_skip_wait_until
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DirectStateSkip(WorkflowState[None]):
|
|
27
|
+
def execute(
|
|
28
|
+
self,
|
|
29
|
+
ctx: WorkflowContext,
|
|
30
|
+
input: T,
|
|
31
|
+
command_results: CommandResults,
|
|
32
|
+
persistence: Persistence,
|
|
33
|
+
communication: Communication,
|
|
34
|
+
) -> StateDecision:
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DirectStateNotSkip(WorkflowState[int]):
|
|
39
|
+
def wait_until(
|
|
40
|
+
self,
|
|
41
|
+
ctx: WorkflowContext,
|
|
42
|
+
input: int,
|
|
43
|
+
persistence: Persistence,
|
|
44
|
+
communication: Communication,
|
|
45
|
+
) -> CommandRequest:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
def execute(
|
|
49
|
+
self,
|
|
50
|
+
ctx: WorkflowContext,
|
|
51
|
+
input: int,
|
|
52
|
+
command_results: CommandResults,
|
|
53
|
+
persistence: Persistence,
|
|
54
|
+
communication: Communication,
|
|
55
|
+
) -> StateDecision:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IndirectStateSkip(DirectStateSkip):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IndirectStateNotSkip(DirectStateSkip):
|
|
64
|
+
def wait_until(
|
|
65
|
+
self,
|
|
66
|
+
ctx: WorkflowContext,
|
|
67
|
+
input: T,
|
|
68
|
+
persistence: Persistence,
|
|
69
|
+
communication: Communication,
|
|
70
|
+
) -> CommandRequest:
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestSkipWaitUntil(unittest.TestCase):
|
|
75
|
+
def test_should_skip_wait_until(self) -> None:
|
|
76
|
+
direct_skip = DirectStateSkip()
|
|
77
|
+
direct_not_skip = DirectStateNotSkip()
|
|
78
|
+
indirect_skip = IndirectStateSkip()
|
|
79
|
+
indirect_not_skip = IndirectStateNotSkip()
|
|
80
|
+
|
|
81
|
+
assert should_skip_wait_until(direct_skip)
|
|
82
|
+
assert should_skip_wait_until(indirect_skip)
|
|
83
|
+
|
|
84
|
+
assert not should_skip_wait_until(direct_not_skip)
|
|
85
|
+
assert not should_skip_wait_until(indirect_not_skip)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.tests.worker_server import registry
|
|
21
|
+
from dex.tests.workflows.recovery_workflow import RecoveryWorkflow
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Test(unittest.TestCase):
|
|
25
|
+
@classmethod
|
|
26
|
+
def setUpClass(cls):
|
|
27
|
+
cls.client = Client(registry)
|
|
28
|
+
|
|
29
|
+
def test_workflow_recovery(self):
|
|
30
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
31
|
+
self.client.start_workflow(RecoveryWorkflow, wf_id, 10)
|
|
32
|
+
result = self.client.wait_for_workflow_completion(wf_id, str)
|
|
33
|
+
assert result == "done"
|
dex/tests/test_timer.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.tests.worker_server import registry
|
|
21
|
+
from dex.tests.workflows.timer_workflow import TimerWorkflow, WaitState
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestTimer(unittest.TestCase):
|
|
25
|
+
@classmethod
|
|
26
|
+
def setUpClass(cls):
|
|
27
|
+
cls.client = Client(registry)
|
|
28
|
+
|
|
29
|
+
def test_timer(self):
|
|
30
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
31
|
+
|
|
32
|
+
self.client.start_workflow(TimerWorkflow, wf_id, 100, 5)
|
|
33
|
+
time.sleep(1)
|
|
34
|
+
self.client.skip_timer_at_command_index(wf_id, WaitState)
|
|
35
|
+
start_ms = time.time_ns() / 1000000
|
|
36
|
+
self.client.wait_for_workflow_completion(wf_id, None)
|
|
37
|
+
elapsed_ms = time.time_ns() / 1000000 - start_ms
|
|
38
|
+
assert (
|
|
39
|
+
3000 <= elapsed_ms <= 6000
|
|
40
|
+
), f"expected 5000 ms timer, actual is {elapsed_ms}"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.tests.worker_server import registry
|
|
21
|
+
from dex.tests.workflows.wait_for_state_with_wait_for_key_workflow import (
|
|
22
|
+
WaitForStateWithWaitForKeyWorkflow,
|
|
23
|
+
WaitForStateWithWaitForKeyState2,
|
|
24
|
+
)
|
|
25
|
+
from dex.workflow_options import WorkflowOptions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestWaitForStateWithWaitForKey(unittest.TestCase):
|
|
29
|
+
@classmethod
|
|
30
|
+
def setUpClass(cls):
|
|
31
|
+
cls.client = Client(registry)
|
|
32
|
+
|
|
33
|
+
def test_wait_for_state_with_wait_for_key_workflow(self):
|
|
34
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
35
|
+
|
|
36
|
+
wf_opts = WorkflowOptions()
|
|
37
|
+
wf_opts.add_wait_for_completion_state_ids(WaitForStateWithWaitForKeyState2)
|
|
38
|
+
|
|
39
|
+
self.client.start_workflow(
|
|
40
|
+
WaitForStateWithWaitForKeyWorkflow, wf_id, 100, None, wf_opts
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.client.wait_for_state_execution_completion_with_wait_for_key(
|
|
44
|
+
WaitForStateWithWaitForKeyState2, wf_id, "testKey"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.client.wait_for_workflow_completion(wf_id)
|
|
48
|
+
|
|
49
|
+
# TODO: Improve this test by verifying both workflows completed with describe_workflow method
|
|
50
|
+
# https://github.com/superdurable/dex-python-sdk/issues/22
|
|
51
|
+
#
|
|
52
|
+
# child_wf_id =
|
|
53
|
+
#
|
|
54
|
+
# wf_info = self.client.describe_workflow(wf_id);
|
|
55
|
+
# assert wf_info.workflow_status == WorkflowStatus.COMPLETED
|
|
56
|
+
#
|
|
57
|
+
# child_wf_info = self.client.describe_workflow(child_wf_id);
|
|
58
|
+
# assert child_wf_info.workflow_status == WorkflowStatus.COMPLETED
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.errors import (
|
|
21
|
+
WorkflowAlreadyStartedError,
|
|
22
|
+
WorkflowCanceled,
|
|
23
|
+
WorkflowFailed,
|
|
24
|
+
WorkflowNotExistsError,
|
|
25
|
+
WorkflowStillRunningError,
|
|
26
|
+
WorkflowTerminated,
|
|
27
|
+
WorkflowTimeout,
|
|
28
|
+
)
|
|
29
|
+
from dex.dex_api.models import WorkflowStopType
|
|
30
|
+
from dex.stop_workflow_options import StopWorkflowOptions
|
|
31
|
+
from dex.tests.worker_server import registry
|
|
32
|
+
from dex.tests.workflows.wait_internal_channel_workflow import (
|
|
33
|
+
WaitInternalChannelWorkflow,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestWorkflowErrors(unittest.TestCase):
|
|
38
|
+
@classmethod
|
|
39
|
+
def setUpClass(cls):
|
|
40
|
+
cls.client = Client(registry)
|
|
41
|
+
|
|
42
|
+
def test_workflow_timeout(self):
|
|
43
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
44
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 1)
|
|
45
|
+
with self.assertRaises(WorkflowTimeout):
|
|
46
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
47
|
+
with self.assertRaises(WorkflowNotExistsError):
|
|
48
|
+
self.client.wait_for_workflow_completion("invalid_id", str)
|
|
49
|
+
|
|
50
|
+
def test_workflow_still_running_when_wait(self):
|
|
51
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
52
|
+
# client_options = ClientOptions.local_default()
|
|
53
|
+
# client_options.api_timeout = 5
|
|
54
|
+
# TODO using a shorter api timeout will throw a different timeout error, it's better to unify it
|
|
55
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 61)
|
|
56
|
+
|
|
57
|
+
with self.assertRaises(WorkflowAlreadyStartedError):
|
|
58
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 61)
|
|
59
|
+
|
|
60
|
+
with self.assertRaises(WorkflowStillRunningError):
|
|
61
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
62
|
+
|
|
63
|
+
def test_workflow_canceled(self):
|
|
64
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
65
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
|
|
66
|
+
self.client.stop_workflow(wf_id)
|
|
67
|
+
with self.assertRaises(WorkflowCanceled):
|
|
68
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
69
|
+
|
|
70
|
+
def test_workflow_terminated(self):
|
|
71
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
72
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
|
|
73
|
+
self.client.stop_workflow(
|
|
74
|
+
wf_id,
|
|
75
|
+
StopWorkflowOptions(
|
|
76
|
+
workflow_stop_type=WorkflowStopType.TERMINATE, reason="test"
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
with self.assertRaises(WorkflowTerminated):
|
|
80
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
81
|
+
|
|
82
|
+
def test_workflow_failed(self):
|
|
83
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
84
|
+
self.client.start_workflow(WaitInternalChannelWorkflow, wf_id, 10)
|
|
85
|
+
self.client.stop_workflow(
|
|
86
|
+
wf_id,
|
|
87
|
+
StopWorkflowOptions(
|
|
88
|
+
workflow_stop_type=WorkflowStopType.FAIL, reason="test"
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
with self.assertRaises(WorkflowFailed):
|
|
92
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
@@ -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
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.dex_api.models import IDReusePolicy
|
|
21
|
+
from dex.dex_api.models import (
|
|
22
|
+
PersistenceLoadingPolicy,
|
|
23
|
+
PersistenceLoadingType,
|
|
24
|
+
WorkflowStateOptions as IdlWorkflowStateOptions,
|
|
25
|
+
RetryPolicy,
|
|
26
|
+
WaitUntilApiFailurePolicy,
|
|
27
|
+
)
|
|
28
|
+
from dex.tests.worker_server import registry
|
|
29
|
+
from dex.tests.workflows.state_options_workflow import (
|
|
30
|
+
StateOptionsWorkflow1,
|
|
31
|
+
StateOptionsWorkflow2,
|
|
32
|
+
)
|
|
33
|
+
from dex.workflow_options import WorkflowOptions
|
|
34
|
+
from dex.workflow_state_options import WorkflowStateOptions, _to_idl_state_options
|
|
35
|
+
from ..errors import WorkflowFailed
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestWorkflowStateOptions(unittest.TestCase):
|
|
39
|
+
@classmethod
|
|
40
|
+
def setUpClass(cls):
|
|
41
|
+
cls.client = Client(registry)
|
|
42
|
+
|
|
43
|
+
def test_convert_to_idl(self):
|
|
44
|
+
empty_idl = IdlWorkflowStateOptions()
|
|
45
|
+
assert empty_idl == _to_idl_state_options(False, None, {})
|
|
46
|
+
|
|
47
|
+
non_empty = WorkflowStateOptions(
|
|
48
|
+
"state-id",
|
|
49
|
+
search_attributes_loading_policy=PersistenceLoadingPolicy(
|
|
50
|
+
persistence_loading_type=PersistenceLoadingType.LOAD_ALL_WITHOUT_LOCKING
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
non_empty_idl = IdlWorkflowStateOptions(
|
|
54
|
+
skip_wait_until=True,
|
|
55
|
+
search_attributes_loading_policy=PersistenceLoadingPolicy(
|
|
56
|
+
persistence_loading_type=PersistenceLoadingType.LOAD_ALL_WITHOUT_LOCKING
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
assert non_empty_idl == _to_idl_state_options(True, non_empty, {})
|
|
60
|
+
non_empty.state_id = "state-id-2"
|
|
61
|
+
assert non_empty.state_id == "state-id-2"
|
|
62
|
+
|
|
63
|
+
"""Test that proceed_to_execute_when_wait_until_retry_exhausted correctly handles both enum values."""
|
|
64
|
+
|
|
65
|
+
def test_proceed_to_execute_when_wait_until_retry_exhausted(self):
|
|
66
|
+
retry_policy = RetryPolicy(maximum_attempts=1)
|
|
67
|
+
|
|
68
|
+
# Test PROCEED_ON_FAILURE
|
|
69
|
+
options_proceed = WorkflowStateOptions(
|
|
70
|
+
proceed_to_execute_when_wait_until_retry_exhausted=WaitUntilApiFailurePolicy.PROCEED_ON_FAILURE,
|
|
71
|
+
wait_until_api_retry_policy=retry_policy,
|
|
72
|
+
)
|
|
73
|
+
result_proceed = _to_idl_state_options(False, options_proceed, {})
|
|
74
|
+
assert (
|
|
75
|
+
result_proceed.wait_until_api_failure_policy
|
|
76
|
+
== WaitUntilApiFailurePolicy.PROCEED_ON_FAILURE
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Test FAIL_WORKFLOW_ON_FAILURE
|
|
80
|
+
options_fail = WorkflowStateOptions(
|
|
81
|
+
proceed_to_execute_when_wait_until_retry_exhausted=WaitUntilApiFailurePolicy.FAIL_WORKFLOW_ON_FAILURE,
|
|
82
|
+
wait_until_api_retry_policy=retry_policy,
|
|
83
|
+
)
|
|
84
|
+
result_fail = _to_idl_state_options(False, options_fail, {})
|
|
85
|
+
assert (
|
|
86
|
+
result_fail.wait_until_api_failure_policy
|
|
87
|
+
== WaitUntilApiFailurePolicy.FAIL_WORKFLOW_ON_FAILURE
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Test with None/unset value
|
|
91
|
+
options = WorkflowStateOptions()
|
|
92
|
+
result = _to_idl_state_options(False, options, {})
|
|
93
|
+
# By default, wait_until_api_failure_policy should not be set when proceed_to_execute_when_wait_until_retry_exhausted is None
|
|
94
|
+
# The DEX service will use FAIL_WORKFLOW_ON_FAILURE by default
|
|
95
|
+
from dex.dex_api.types import Unset
|
|
96
|
+
|
|
97
|
+
self.assertTrue(isinstance(result.wait_until_api_failure_policy, Unset))
|
|
98
|
+
|
|
99
|
+
def test_proceed_on_failure(self):
|
|
100
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
101
|
+
self.client.start_workflow(
|
|
102
|
+
StateOptionsWorkflow1,
|
|
103
|
+
wf_id,
|
|
104
|
+
10,
|
|
105
|
+
"input",
|
|
106
|
+
WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
|
|
107
|
+
)
|
|
108
|
+
output = self.client.wait_for_workflow_completion(wf_id)
|
|
109
|
+
|
|
110
|
+
assert output == "InitState1_execute_completed"
|
|
111
|
+
|
|
112
|
+
def test_fail_workflow_on_failure(self):
|
|
113
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
114
|
+
self.client.start_workflow(
|
|
115
|
+
StateOptionsWorkflow2,
|
|
116
|
+
wf_id,
|
|
117
|
+
10,
|
|
118
|
+
"input",
|
|
119
|
+
WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
with self.assertRaises(WorkflowFailed):
|
|
123
|
+
self.client.wait_for_workflow_completion(wf_id, str)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (c) 2022-2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import inspect
|
|
16
|
+
import time
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
from dex.client import Client
|
|
20
|
+
from dex.workflow_options import WorkflowOptions
|
|
21
|
+
|
|
22
|
+
from dex.dex_api.models import IDReusePolicy
|
|
23
|
+
from dex.tests.worker_server import registry
|
|
24
|
+
|
|
25
|
+
from dex.tests.workflows.state_options_override_workflow import (
|
|
26
|
+
StateOptionsOverrideWorkflow,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestStateOptionsOverrideWorkflow(unittest.TestCase):
|
|
31
|
+
@classmethod
|
|
32
|
+
def setUpClass(cls):
|
|
33
|
+
cls.client = Client(registry)
|
|
34
|
+
|
|
35
|
+
def test_override(self):
|
|
36
|
+
wf_id = f"{inspect.currentframe().f_code.co_name}-{time.time_ns()}"
|
|
37
|
+
self.client.start_workflow(
|
|
38
|
+
StateOptionsOverrideWorkflow,
|
|
39
|
+
wf_id,
|
|
40
|
+
10,
|
|
41
|
+
"input",
|
|
42
|
+
WorkflowOptions(workflow_id_reuse_policy=IDReusePolicy.DISALLOW_REUSE),
|
|
43
|
+
)
|
|
44
|
+
output = self.client.wait_for_workflow_completion(wf_id)
|
|
45
|
+
|
|
46
|
+
assert (
|
|
47
|
+
output
|
|
48
|
+
== "input_InitState_waitUntil_completed_InitState_execute_completed_NonInitState_execute_completed"
|
|
49
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
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 threading import Thread
|
|
16
|
+
|
|
17
|
+
from flask import Flask, request
|
|
18
|
+
|
|
19
|
+
from dex.dex_api.models import (
|
|
20
|
+
WorkflowStateExecuteRequest,
|
|
21
|
+
WorkflowStateWaitUntilRequest,
|
|
22
|
+
WorkflowWorkerRpcRequest,
|
|
23
|
+
)
|
|
24
|
+
from dex.tests import registry
|
|
25
|
+
from dex.worker_service import (
|
|
26
|
+
WorkerService,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# NOTE: set this to true when debugging(using breakpoints)
|
|
30
|
+
# so that it keep the thread running so that we can see the error in history
|
|
31
|
+
debug_mode: bool = False
|
|
32
|
+
|
|
33
|
+
_flask_app = Flask(__name__)
|
|
34
|
+
|
|
35
|
+
_worker_service = WorkerService(registry)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@_flask_app.route("/")
|
|
39
|
+
def index():
|
|
40
|
+
return "hello"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@_flask_app.route(WorkerService.api_path_workflow_state_wait_until, methods=["POST"])
|
|
44
|
+
def handle_wait_until():
|
|
45
|
+
req = WorkflowStateWaitUntilRequest.from_dict(request.json)
|
|
46
|
+
resp = _worker_service.handle_workflow_state_wait_until(req)
|
|
47
|
+
return resp.to_dict()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@_flask_app.route(WorkerService.api_path_workflow_state_execute, methods=["POST"])
|
|
51
|
+
def handle_execute():
|
|
52
|
+
req = WorkflowStateExecuteRequest.from_dict(request.json)
|
|
53
|
+
resp = _worker_service.handle_workflow_state_execute(req)
|
|
54
|
+
return resp.to_dict()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@_flask_app.route(WorkerService.api_path_workflow_worker_rpc, methods=["POST"])
|
|
58
|
+
def handle_rpc():
|
|
59
|
+
req = WorkflowWorkerRpcRequest.from_dict(request.json)
|
|
60
|
+
resp = _worker_service.handle_workflow_worker_rpc(req)
|
|
61
|
+
return resp.to_dict()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# this handler is extremely useful for debugging Dex
|
|
65
|
+
# the WebUI will be able to show you the error with stacktrace
|
|
66
|
+
@_flask_app.errorhandler(Exception)
|
|
67
|
+
def internal_error(exception):
|
|
68
|
+
return _worker_service.handle_worker_error(exception), 500
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_webApp = Thread(target=_flask_app.run, args=("0.0.0.0", 8802))
|
|
72
|
+
# when debugging, keep the thread running so that we can see the error in history
|
|
73
|
+
_webApp.setDaemon(not debug_mode)
|
|
74
|
+
_webApp.start()
|
|
@@ -0,0 +1,48 @@
|
|
|
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.command_results import CommandResults
|
|
18
|
+
from dex.communication import Communication
|
|
19
|
+
from dex.dex_api.models import RetryPolicy
|
|
20
|
+
from dex.persistence import Persistence
|
|
21
|
+
from dex.state_decision import StateDecision
|
|
22
|
+
from dex.state_schema import StateSchema
|
|
23
|
+
from dex.workflow import ObjectWorkflow
|
|
24
|
+
from dex.workflow_context import WorkflowContext
|
|
25
|
+
from dex.workflow_state import T, WorkflowState
|
|
26
|
+
from dex.workflow_state_options import WorkflowStateOptions
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AbnormalExitState1(WorkflowState[Union[int, str]]):
|
|
30
|
+
def execute(
|
|
31
|
+
self,
|
|
32
|
+
ctx: WorkflowContext,
|
|
33
|
+
input: T,
|
|
34
|
+
command_results: CommandResults,
|
|
35
|
+
persistence: Persistence,
|
|
36
|
+
communication: Communication,
|
|
37
|
+
) -> StateDecision:
|
|
38
|
+
raise RuntimeError("abnormal exit state")
|
|
39
|
+
|
|
40
|
+
def get_state_options(self) -> WorkflowStateOptions:
|
|
41
|
+
return WorkflowStateOptions(
|
|
42
|
+
execute_api_retry_policy=RetryPolicy(maximum_attempts=1)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AbnormalExitWorkflow(ObjectWorkflow):
|
|
47
|
+
def get_workflow_states(self) -> StateSchema:
|
|
48
|
+
return StateSchema.with_starting_state(AbnormalExitState1())
|
|
@@ -0,0 +1,69 @@
|
|
|
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.command_request import CommandRequest, TimerCommand
|
|
18
|
+
from dex.command_results import CommandResults
|
|
19
|
+
from dex.communication import Communication
|
|
20
|
+
from dex.persistence import Persistence
|
|
21
|
+
from dex.state_decision import StateDecision
|
|
22
|
+
from dex.state_schema import StateSchema
|
|
23
|
+
from dex.workflow import ObjectWorkflow
|
|
24
|
+
from dex.workflow_context import WorkflowContext
|
|
25
|
+
from dex.workflow_state import T, WorkflowState
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class State1(WorkflowState[Union[int, str]]):
|
|
29
|
+
def wait_until(
|
|
30
|
+
self,
|
|
31
|
+
ctx: WorkflowContext,
|
|
32
|
+
input: T,
|
|
33
|
+
persistence: Persistence,
|
|
34
|
+
communication: Communication,
|
|
35
|
+
) -> CommandRequest:
|
|
36
|
+
if input != "input":
|
|
37
|
+
raise RuntimeError("input is incorrect")
|
|
38
|
+
return CommandRequest.for_all_command_completed(
|
|
39
|
+
TimerCommand.by_seconds(1),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def execute(
|
|
43
|
+
self,
|
|
44
|
+
ctx: WorkflowContext,
|
|
45
|
+
input: T,
|
|
46
|
+
command_results: CommandResults,
|
|
47
|
+
persistence: Persistence,
|
|
48
|
+
communication: Communication,
|
|
49
|
+
) -> StateDecision:
|
|
50
|
+
if input != "input":
|
|
51
|
+
raise RuntimeError("input is incorrect")
|
|
52
|
+
return StateDecision.single_next_state(State2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class State2(WorkflowState[None]):
|
|
56
|
+
def execute(
|
|
57
|
+
self,
|
|
58
|
+
ctx: WorkflowContext,
|
|
59
|
+
input: T,
|
|
60
|
+
command_results: CommandResults,
|
|
61
|
+
persistence: Persistence,
|
|
62
|
+
communication: Communication,
|
|
63
|
+
) -> StateDecision:
|
|
64
|
+
return StateDecision.graceful_complete_workflow("done")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BasicWorkflow(ObjectWorkflow):
|
|
68
|
+
def get_workflow_states(self) -> StateSchema:
|
|
69
|
+
return StateSchema.with_starting_state(State1(), State2())
|