dex-python-sdk 0.0.2__cp311-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. dex/__init__.py +146 -0
  2. dex/_grpc_errors.py +73 -0
  3. dex/_invocation_context.py +269 -0
  4. dex/_native.pyd +0 -0
  5. dex/_native.pyi +20 -0
  6. dex/_utils.py +18 -0
  7. dex/_value_hydrator.py +223 -0
  8. dex/_value_mapper.py +174 -0
  9. dex/_worker_dispatcher.py +451 -0
  10. dex/_worker_service.py +60 -0
  11. dex/attribute.py +87 -0
  12. dex/blob_cache.py +73 -0
  13. dex/channel.py +165 -0
  14. dex/client.py +715 -0
  15. dex/client_options.py +19 -0
  16. dex/codec.py +286 -0
  17. dex/command_request.py +120 -0
  18. dex/command_results.py +107 -0
  19. dex/communication.py +136 -0
  20. dex/communication_schema.py +54 -0
  21. dex/condition.py +74 -0
  22. dex/context.py +86 -0
  23. dex/data_attributes.py +70 -0
  24. dex/dexpb/__init__.py +1 -0
  25. dex/dexpb/dex_pb2.py +381 -0
  26. dex/dexpb/dex_pb2.pyi +1734 -0
  27. dex/dexpb/dex_pb2_grpc.py +1298 -0
  28. dex/errors.py +109 -0
  29. dex/flow.py +456 -0
  30. dex/flow_config.py +29 -0
  31. dex/flow_info.py +51 -0
  32. dex/flow_options.py +122 -0
  33. dex/object_encoder.py +799 -0
  34. dex/persistence.py +89 -0
  35. dex/persistence_options.py +12 -0
  36. dex/persistence_schema.py +51 -0
  37. dex/py.typed +1 -0
  38. dex/registry.py +204 -0
  39. dex/reset_workflow_type_and_options.py +67 -0
  40. dex/rpc.py +93 -0
  41. dex/runtime_errors.py +81 -0
  42. dex/search_attributes.py +184 -0
  43. dex/state_decision.py +153 -0
  44. dex/state_execution_locals.py +66 -0
  45. dex/state_movement.py +115 -0
  46. dex/state_schema.py +48 -0
  47. dex/step.py +194 -0
  48. dex/step_execution.py +42 -0
  49. dex/stop_workflow_options.py +18 -0
  50. dex/tests/__init__.py +80 -0
  51. dex/tests/dex-service-env/.env +7 -0
  52. dex/tests/dex-service-env/docker-compose-init.sh +44 -0
  53. dex/tests/dex-service-env/docker-compose.yml +97 -0
  54. dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
  55. dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
  56. dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
  57. dex/tests/test_abnormal_exit_workflow.py +43 -0
  58. dex/tests/test_basic_workflow.py +70 -0
  59. dex/tests/test_conditional_complete.py +50 -0
  60. dex/tests/test_describe_workflow.py +40 -0
  61. dex/tests/test_empty_data_decodes_properly.py +74 -0
  62. dex/tests/test_internal_channel.py +28 -0
  63. dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
  64. dex/tests/test_persistence_data_attributes.py +62 -0
  65. dex/tests/test_persistence_search_attributes.py +127 -0
  66. dex/tests/test_persistence_state_execution_locals.py +38 -0
  67. dex/tests/test_rpc.py +64 -0
  68. dex/tests/test_rpc_with_memo.py +195 -0
  69. dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
  70. dex/tests/test_signal.py +51 -0
  71. dex/tests/test_skip_wait_until.py +76 -0
  72. dex/tests/test_state_failure_recovery.py +28 -0
  73. dex/tests/test_timer.py +35 -0
  74. dex/tests/test_wait_for_state_execution_completion.py +53 -0
  75. dex/tests/test_workflow_errors.py +87 -0
  76. dex/tests/test_workflow_state_options.py +118 -0
  77. dex/tests/test_workflow_state_options_override.py +44 -0
  78. dex/tests/worker_server.py +64 -0
  79. dex/tests/workflows/abnormal_exit_workflow.py +42 -0
  80. dex/tests/workflows/basic_workflow.py +62 -0
  81. dex/tests/workflows/conditional_complete_workflow.py +95 -0
  82. dex/tests/workflows/describe_workflow.py +46 -0
  83. dex/tests/workflows/empty_data_workflow.py +45 -0
  84. dex/tests/workflows/internal_channel_workflow.py +129 -0
  85. dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
  86. dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
  87. dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
  88. dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
  89. dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
  90. dex/tests/workflows/recovery_workflow.py +82 -0
  91. dex/tests/workflows/rpc_memo_workflow.py +231 -0
  92. dex/tests/workflows/rpc_workflow.py +117 -0
  93. dex/tests/workflows/state_options_override_workflow.py +93 -0
  94. dex/tests/workflows/state_options_workflow.py +84 -0
  95. dex/tests/workflows/timer_workflow.py +46 -0
  96. dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
  97. dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
  98. dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
  99. dex/tests/workflows/wait_signal_workflow.py +147 -0
  100. dex/timer.py +21 -0
  101. dex/type_store.py +99 -0
  102. dex/unregistered_client.py +585 -0
  103. dex/utils/__init__.py +3 -0
  104. dex/utils/dex_typing.py +25 -0
  105. dex/utils/persistence_utils.py +32 -0
  106. dex/wait.py +49 -0
  107. dex/worker.py +121 -0
  108. dex/worker_options.py +22 -0
  109. dex/worker_service.py +432 -0
  110. dex/workflow.py +79 -0
  111. dex/workflow_context.py +44 -0
  112. dex/workflow_info.py +16 -0
  113. dex/workflow_options.py +74 -0
  114. dex/workflow_state.py +123 -0
  115. dex/workflow_state_options.py +154 -0
  116. dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
  117. dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
  118. dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
  119. dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
  120. dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
  121. dex_python_sdk-0.0.2.dist-info/sboms/dex-blob-cache-python.cyclonedx.json +2406 -0
dex/client_options.py ADDED
@@ -0,0 +1,19 @@
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
+
13
+ from dex.worker_options import WorkerTarget
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ClientOptions:
18
+ server_address: str = "localhost:8801"
19
+ worker_target: WorkerTarget | None = None
dex/codec.py ADDED
@@ -0,0 +1,286 @@
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 __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ from dataclasses import asdict, dataclass, field, fields, is_dataclass
14
+ from datetime import datetime
15
+ from enum import Enum
16
+ from typing import (
17
+ Any,
18
+ Callable,
19
+ Generic,
20
+ Mapping,
21
+ Protocol,
22
+ Sequence,
23
+ TypeVar,
24
+ cast,
25
+ get_args,
26
+ get_origin,
27
+ get_type_hints,
28
+ )
29
+
30
+ ValueT = TypeVar("ValueT")
31
+
32
+
33
+ class WireKind(Enum):
34
+ STRING = "string"
35
+ BOOL = "bool"
36
+ INT64 = "int64"
37
+ DOUBLE = "double"
38
+ BYTES = "bytes"
39
+ JSON = "json"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Value:
44
+ kind: WireKind
45
+ data: str | bool | int | float | bytes
46
+
47
+
48
+ class Codec(Protocol[ValueT]):
49
+ @property
50
+ def type_name(self) -> str: ...
51
+
52
+ @property
53
+ def wire_kind(self) -> WireKind: ...
54
+
55
+ def encode(self, value: ValueT) -> Value: ...
56
+
57
+ def decode(self, value: Value) -> ValueT: ...
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class _ScalarCodec(Generic[ValueT]):
62
+ type_name: str
63
+ wire_kind: WireKind
64
+ expected_type: type[Any]
65
+ validator: Callable[[ValueT], None] | None = None
66
+
67
+ def encode(self, value: ValueT) -> Value:
68
+ if type(value) is not self.expected_type:
69
+ raise TypeError(
70
+ f"{self.type_name} requires {self.expected_type.__name__}, "
71
+ f"got {type(value).__name__}"
72
+ )
73
+ if self.validator is not None:
74
+ self.validator(value)
75
+ return Value(self.wire_kind, cast(str | bool | int | float | bytes, value))
76
+
77
+ def decode(self, value: Value) -> ValueT:
78
+ if value.kind is not self.wire_kind:
79
+ raise TypeError(
80
+ f"{self.type_name} cannot decode wire kind {value.kind.value}"
81
+ )
82
+ decoded = value.data
83
+ if type(decoded) is not self.expected_type:
84
+ raise TypeError(f"invalid {self.type_name} payload")
85
+ typed = cast(ValueT, decoded)
86
+ if self.validator is not None:
87
+ self.validator(typed)
88
+ return typed
89
+
90
+
91
+ def _validate_int64(value: int) -> None:
92
+ if value < -(2**63) or value > 2**63 - 1:
93
+ raise OverflowError(f"integer {value} exceeds int64")
94
+
95
+
96
+ def _validate_double(value: float) -> None:
97
+ if not math.isfinite(value):
98
+ raise ValueError("non-finite floating-point values are unsupported")
99
+
100
+
101
+ STRING: Codec[str] = _ScalarCodec("str", WireKind.STRING, str)
102
+ BOOL: Codec[bool] = _ScalarCodec("bool", WireKind.BOOL, bool)
103
+ INT64: Codec[int] = _ScalarCodec("int", WireKind.INT64, int, _validate_int64)
104
+ DOUBLE: Codec[float] = _ScalarCodec("float", WireKind.DOUBLE, float, _validate_double)
105
+ BYTES: Codec[bytes] = _ScalarCodec("bytes", WireKind.BYTES, bytes)
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class _NoneCodec:
110
+ type_name: str = "None"
111
+ wire_kind: WireKind = WireKind.JSON
112
+
113
+ def encode(self, value: None) -> Value:
114
+ if value is not None:
115
+ raise TypeError("None codec requires None")
116
+ return Value(WireKind.JSON, "null")
117
+
118
+ def decode(self, value: Value) -> None:
119
+ if value.kind is not WireKind.JSON or value.data != "null":
120
+ raise TypeError("None codec requires JSON null")
121
+ return None
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class _DateTimeCodec:
126
+ type_name: str = "datetime"
127
+ wire_kind: WireKind = WireKind.STRING
128
+
129
+ def encode(self, value: datetime) -> Value:
130
+ if not isinstance(value, datetime):
131
+ raise TypeError("datetime codec requires datetime")
132
+ return Value(WireKind.STRING, value.isoformat())
133
+
134
+ def decode(self, value: Value) -> datetime:
135
+ if value.kind is not WireKind.STRING or not isinstance(value.data, str):
136
+ raise TypeError("datetime codec requires a string value")
137
+ return datetime.fromisoformat(value.data)
138
+
139
+
140
+ _NONE: Codec[None] = _NoneCodec()
141
+ _DATETIME: Codec[datetime] = _DateTimeCodec()
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class JsonCodec(Generic[ValueT]):
146
+ type_name: str
147
+ decoder: Callable[[Any], ValueT]
148
+ encoder: Callable[[ValueT], Any] = field(default=lambda value: value)
149
+ expected_type: object | None = None
150
+ wire_kind: WireKind = field(default=WireKind.JSON, init=False)
151
+
152
+ def encode(self, value: ValueT) -> Value:
153
+ if self.expected_type is not None and not _matches_type(
154
+ value, self.expected_type
155
+ ):
156
+ raise TypeError(
157
+ f"{self.type_name} requires {_type_name(self.expected_type)}, "
158
+ f"got {type(value).__name__}"
159
+ )
160
+ payload = json.dumps(
161
+ self.encoder(value),
162
+ allow_nan=False,
163
+ separators=(",", ":"),
164
+ sort_keys=True,
165
+ )
166
+ return Value(WireKind.JSON, payload)
167
+
168
+ def decode(self, value: Value) -> ValueT:
169
+ if value.kind is not WireKind.JSON or not isinstance(value.data, str):
170
+ raise TypeError(f"{self.type_name} requires a JSON value")
171
+ return self.decoder(json.loads(value.data))
172
+
173
+
174
+ class CodecRegistry:
175
+ def __init__(self, codecs: Mapping[object, Codec[Any]] | None = None) -> None:
176
+ self._codecs = dict(codecs or {})
177
+
178
+ def resolve(self, type_hint: object) -> Codec[Any]:
179
+ custom = self._codecs.get(type_hint)
180
+ if custom is not None:
181
+ return custom
182
+ builtins: dict[object, Codec[Any]] = {
183
+ str: STRING,
184
+ bool: BOOL,
185
+ int: INT64,
186
+ float: DOUBLE,
187
+ bytes: BYTES,
188
+ type(None): _NONE,
189
+ datetime: _DATETIME,
190
+ }
191
+ builtin = builtins.get(type_hint)
192
+ if builtin is not None:
193
+ return builtin
194
+ if _supports_automatic_json(type_hint):
195
+ return JsonCodec(
196
+ _type_name(type_hint),
197
+ lambda value: _decode_json_value(value, type_hint),
198
+ _encode_json_value,
199
+ type_hint,
200
+ )
201
+ raise TypeError(
202
+ f"no codec for {_type_name(type_hint)}; register one in CodecRegistry"
203
+ )
204
+
205
+
206
+ def _supports_automatic_json(type_hint: object) -> bool:
207
+ origin = get_origin(type_hint)
208
+ return (
209
+ isinstance(type_hint, type)
210
+ and (is_dataclass(type_hint) or issubclass(type_hint, Enum))
211
+ ) or origin in (list, tuple, dict, Mapping, Sequence)
212
+
213
+
214
+ def _type_name(type_hint: object) -> str:
215
+ return getattr(type_hint, "__qualname__", str(type_hint))
216
+
217
+
218
+ def _matches_type(value: object, type_hint: object) -> bool:
219
+ origin = get_origin(type_hint)
220
+ arguments = get_args(type_hint)
221
+ if origin is list:
222
+ return isinstance(value, list) and all(
223
+ _matches_type(item, arguments[0]) for item in value
224
+ )
225
+ if origin is tuple:
226
+ return isinstance(value, tuple) and all(
227
+ _matches_type(item, arguments[0]) for item in value
228
+ )
229
+ if origin in (dict, Mapping):
230
+ return isinstance(value, Mapping) and all(
231
+ _matches_type(key, arguments[0]) and _matches_type(item, arguments[1])
232
+ for key, item in value.items()
233
+ )
234
+ return isinstance(type_hint, type) and type(value) is type_hint
235
+
236
+
237
+ def _encode_json_value(value: Any) -> Any:
238
+ if is_dataclass(value) and not isinstance(value, type):
239
+ return asdict(value)
240
+ if isinstance(value, datetime):
241
+ return value.isoformat()
242
+ if isinstance(value, Enum):
243
+ return value.value
244
+ if isinstance(value, Mapping):
245
+ return {key: _encode_json_value(item) for key, item in value.items()}
246
+ if isinstance(value, (list, tuple)):
247
+ return [_encode_json_value(item) for item in value]
248
+ return value
249
+
250
+
251
+ def _decode_json_value(value: Any, type_hint: object) -> Any:
252
+ origin = get_origin(type_hint)
253
+ arguments = get_args(type_hint)
254
+ if isinstance(type_hint, type) and is_dataclass(type_hint):
255
+ if not isinstance(value, Mapping):
256
+ raise TypeError(f"{_type_name(type_hint)} requires a JSON object")
257
+ hints = get_type_hints(type_hint)
258
+ return type_hint(
259
+ **{
260
+ definition.name: _decode_json_value(
261
+ value[definition.name], hints[definition.name]
262
+ )
263
+ for definition in fields(type_hint)
264
+ }
265
+ )
266
+ if type_hint is datetime:
267
+ if not isinstance(value, str):
268
+ raise TypeError("datetime requires a JSON string")
269
+ return datetime.fromisoformat(value)
270
+ if isinstance(type_hint, type) and issubclass(type_hint, Enum):
271
+ return type_hint(value)
272
+ if origin is list:
273
+ return [_decode_json_value(item, arguments[0]) for item in value]
274
+ if origin is tuple:
275
+ return tuple(_decode_json_value(item, arguments[0]) for item in value)
276
+ if origin in (dict, Mapping):
277
+ return {
278
+ _decode_json_value(key, arguments[0]): _decode_json_value(
279
+ item, arguments[1]
280
+ )
281
+ for key, item in value.items()
282
+ }
283
+ if type_hint in (str, bool, int, float):
284
+ if type(value) is not type_hint:
285
+ raise TypeError(f"expected {_type_name(type_hint)}")
286
+ return value
dex/command_request.py ADDED
@@ -0,0 +1,120 @@
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, Union
13
+
14
+ from dex.errors import WorkflowDefinitionError
15
+ from dex.dex_api.models import CommandWaitingType
16
+ from dex.dex_api.models.command_combination import CommandCombination
17
+ from dex.dex_api.models.command_request import (
18
+ CommandRequest as IdlCommandRequest,
19
+ )
20
+ from dex.dex_api.models.inter_state_channel_command import (
21
+ InterStateChannelCommand as IdlInternalChannelCommand,
22
+ )
23
+ from dex.dex_api.models.signal_command import SignalCommand as IdlSignalCommand
24
+ from dex.dex_api.models.timer_command import TimerCommand as IdlTimerCommand
25
+
26
+ @dataclass
27
+ class TimerCommand:
28
+ command_id: str
29
+ duration_seconds: int
30
+
31
+ @classmethod
32
+ def by_seconds(cls, duration_seconds: int, command_id: Optional[str] = None):
33
+ return TimerCommand(
34
+ command_id if command_id is not None else "", duration_seconds
35
+ )
36
+
37
+ @dataclass
38
+ class InternalChannelCommand:
39
+ command_id: str
40
+ channel_name: str
41
+
42
+ @classmethod
43
+ def by_name(cls, channel_name: str, command_id: Optional[str] = None):
44
+ return InternalChannelCommand(
45
+ command_id if command_id is not None else "", channel_name
46
+ )
47
+
48
+ @dataclass
49
+ class SignalChannelCommand:
50
+ command_id: str
51
+ channel_name: str
52
+
53
+ @classmethod
54
+ def by_name(cls, channel_name: str, command_id: Optional[str] = None):
55
+ return SignalChannelCommand(
56
+ command_id if command_id is not None else "",
57
+ channel_name,
58
+ )
59
+
60
+ BaseCommand = Union[TimerCommand, InternalChannelCommand, SignalChannelCommand]
61
+
62
+ @dataclass
63
+ class CommandRequest:
64
+ commands: list[BaseCommand]
65
+ command_waiting_type: CommandWaitingType
66
+ command_combinations: list[CommandCombination]
67
+
68
+ @classmethod
69
+ def for_any_command_completed(cls, *commands: BaseCommand):
70
+ bc = [c for c in commands]
71
+ return CommandRequest(bc, CommandWaitingType.ANY_COMPLETED, [])
72
+
73
+ @classmethod
74
+ def for_all_command_completed(cls, *commands: BaseCommand):
75
+ bc = [c for c in commands]
76
+ return CommandRequest(bc, CommandWaitingType.ALL_COMPLETED, [])
77
+
78
+ @classmethod
79
+ def for_any_command_combination_completed(
80
+ cls, command_combinations_list: list[list[str]], *commands: BaseCommand
81
+ ):
82
+ return CommandRequest(
83
+ list(commands),
84
+ CommandWaitingType.ANY_COMBINATION_COMPLETED,
85
+ [CommandCombination(c) for c in command_combinations_list],
86
+ )
87
+
88
+ @classmethod
89
+ def empty(cls):
90
+ return CommandRequest(list(), CommandWaitingType.ALL_COMPLETED, [])
91
+
92
+ def _to_idl_command_request(request: CommandRequest) -> IdlCommandRequest:
93
+ req = IdlCommandRequest(
94
+ command_waiting_type=request.command_waiting_type,
95
+ )
96
+
97
+ timer_commands = []
98
+ internal_channel_commands = []
99
+ signal_commands = []
100
+ for t in request.commands:
101
+ if isinstance(t, TimerCommand):
102
+ timer_commands.append(IdlTimerCommand(t.duration_seconds, t.command_id))
103
+ elif isinstance(t, InternalChannelCommand):
104
+ internal_channel_commands.append(
105
+ IdlInternalChannelCommand(t.channel_name, t.command_id)
106
+ )
107
+ elif isinstance(t, SignalChannelCommand):
108
+ signal_commands.append(IdlSignalCommand(t.channel_name, t.command_id))
109
+ else:
110
+ raise WorkflowDefinitionError(f"unknown command {t.__class__.__qualname__}")
111
+
112
+ if len(timer_commands) > 0:
113
+ req.timer_commands = timer_commands
114
+ if len(internal_channel_commands) > 0:
115
+ req.inter_state_channel_commands = internal_channel_commands
116
+ if len(signal_commands) > 0:
117
+ req.signal_commands = signal_commands
118
+ if len(request.command_combinations) > 0:
119
+ req.command_combinations = request.command_combinations
120
+ return req
dex/command_results.py ADDED
@@ -0,0 +1,107 @@
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 typing
12
+ from dataclasses import dataclass
13
+ from typing import Any, Union, Optional
14
+
15
+ from dex.errors import WorkflowDefinitionError, NotRegisteredError
16
+ from dex.dex_api.models import (
17
+ ChannelRequestStatus,
18
+ CommandResults as IdlCommandResults,
19
+ TimerStatus,
20
+ )
21
+ from dex.dex_api.types import Unset
22
+ from dex.object_encoder import ObjectEncoder
23
+ from dex.type_store import TypeStore
24
+
25
+ @dataclass
26
+ class TimerCommandResult:
27
+ status: TimerStatus
28
+ command_id: str
29
+
30
+ @dataclass
31
+ class InternalChannelCommandResult:
32
+ channel_name: str
33
+ value: Any
34
+ status: ChannelRequestStatus
35
+ command_id: str
36
+
37
+ @dataclass
38
+ class SignalChannelCommandResult:
39
+ channel_name: str
40
+ value: Any
41
+ status: ChannelRequestStatus
42
+ command_id: str
43
+
44
+ @dataclass
45
+ class CommandResults:
46
+ timer_commands: list[TimerCommandResult]
47
+ internal_channel_commands: list[InternalChannelCommandResult]
48
+ signal_channel_commands: list[SignalChannelCommandResult]
49
+ wait_until_api_succeeded: Optional[bool] = None
50
+
51
+ def from_idl_command_results(
52
+ idl_results: Union[Unset, IdlCommandResults],
53
+ internal_channel_types: TypeStore,
54
+ signal_channel_types: dict[str, typing.Optional[type]],
55
+ object_encoder: ObjectEncoder,
56
+ ) -> CommandResults:
57
+ results = CommandResults(list(), list(), list(), None)
58
+ if isinstance(idl_results, Unset):
59
+ return results
60
+
61
+ if not isinstance(idl_results.timer_results, Unset):
62
+ for timer in idl_results.timer_results:
63
+ results.timer_commands.append(
64
+ TimerCommandResult(timer.timer_status, timer.command_id)
65
+ )
66
+
67
+ if not isinstance(idl_results.inter_state_channel_results, Unset):
68
+ for inter in idl_results.inter_state_channel_results:
69
+
70
+ try:
71
+ val_type = internal_channel_types.get_type(inter.channel_name)
72
+ except NotRegisteredError as exception:
73
+ raise WorkflowDefinitionError(
74
+ "internal channel is not registered: " + inter.channel_name
75
+ ) from exception
76
+
77
+ encoded = object_encoder.decode(inter.value, val_type)
78
+
79
+ results.internal_channel_commands.append(
80
+ InternalChannelCommandResult(
81
+ inter.channel_name,
82
+ encoded,
83
+ inter.request_status,
84
+ inter.command_id,
85
+ )
86
+ )
87
+
88
+ if not isinstance(idl_results.signal_results, Unset):
89
+ for sig in idl_results.signal_results:
90
+ results.signal_channel_commands.append(
91
+ SignalChannelCommandResult(
92
+ sig.signal_channel_name,
93
+ object_encoder.decode(
94
+ sig.signal_value,
95
+ signal_channel_types.get(sig.signal_channel_name),
96
+ ),
97
+ sig.signal_request_status,
98
+ sig.command_id,
99
+ )
100
+ )
101
+
102
+ if not isinstance(idl_results.state_wait_until_failed, Unset):
103
+ # The server will set state_wait_until_failed to true if the waitUntil API failed.
104
+ # Hence, flag inversion is needed here to indicate that the waitUntil API succeeded.
105
+ results.wait_until_api_succeeded = not idl_results.state_wait_until_failed
106
+
107
+ return results
dex/communication.py ADDED
@@ -0,0 +1,136 @@
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 typing import Any, Optional, Union
12
+
13
+ from dex.errors import WorkflowDefinitionError, NotRegisteredError
14
+ from dex.dex_api.models import (
15
+ EncodedObject,
16
+ InterStateChannelPublishing,
17
+ WorkflowWorkerRpcRequestInternalChannelInfos,
18
+ WorkflowWorkerRpcRequestSignalChannelInfos,
19
+ )
20
+ from dex.dex_api.types import Unset
21
+ from dex.object_encoder import ObjectEncoder
22
+ from dex.state_movement import StateMovement
23
+ from dex.type_store import TypeStore
24
+
25
+ class Communication:
26
+ _internal_channel_type_store: TypeStore
27
+ _signal_channel_type_store: dict[str, Optional[type]]
28
+ _object_encoder: ObjectEncoder
29
+ _to_publish_internal_channel: dict[str, list[Union[EncodedObject, Unset]]]
30
+ _state_movements: list[StateMovement]
31
+ _internal_channel_infos: Optional[WorkflowWorkerRpcRequestInternalChannelInfos]
32
+ _signal_channel_infos: Optional[WorkflowWorkerRpcRequestSignalChannelInfos]
33
+
34
+ def __init__(
35
+ self,
36
+ internal_channel_type_store: TypeStore,
37
+ signal_channel_type_store: dict[str, Optional[type]],
38
+ object_encoder: ObjectEncoder,
39
+ internal_channel_infos: Optional[WorkflowWorkerRpcRequestInternalChannelInfos],
40
+ signal_channel_infos: Optional[WorkflowWorkerRpcRequestSignalChannelInfos],
41
+ ):
42
+ self._object_encoder = object_encoder
43
+ self._internal_channel_type_store = internal_channel_type_store
44
+ self._signal_channel_type_store = signal_channel_type_store
45
+ self._to_publish_internal_channel = {}
46
+ self._state_movements = []
47
+ self._internal_channel_infos = internal_channel_infos
48
+ self._signal_channel_infos = signal_channel_infos
49
+
50
+ def trigger_state_execution(self, state: Union[str, type], state_input: Any = None):
51
+ """
52
+
53
+ Args:
54
+ state: the workflowState TODO the type hint should be type[WorkflowState]
55
+ state_input: the input of the state
56
+ """
57
+ movement = StateMovement.create(state, state_input)
58
+ self._state_movements.append(movement)
59
+
60
+ def publish_to_internal_channel(self, channel_name: str, value: Any = None):
61
+ try:
62
+ registered_type = self._internal_channel_type_store.get_type(channel_name)
63
+ except NotRegisteredError as exception:
64
+ raise WorkflowDefinitionError(
65
+ f"InternalChannel channel_name is not defined {channel_name}"
66
+ ) from exception
67
+
68
+ if (
69
+ value is not None
70
+ and registered_type is not None
71
+ and not isinstance(value, registered_type)
72
+ ):
73
+ raise WorkflowDefinitionError(
74
+ f"InternalChannel value is not of type {registered_type}"
75
+ )
76
+ vals = self._to_publish_internal_channel.get(channel_name)
77
+ if vals is None:
78
+ vals = []
79
+ vals.append(self._object_encoder.encode(value))
80
+ self._to_publish_internal_channel[channel_name] = vals
81
+
82
+ def get_to_publishing_internal_channel(self) -> list[InterStateChannelPublishing]:
83
+ pubs = []
84
+ for name, vals in self._to_publish_internal_channel.items():
85
+ for val in vals:
86
+ pubs.append(InterStateChannelPublishing(name, val))
87
+ return pubs
88
+
89
+ def get_to_trigger_state_movements(self) -> list[StateMovement]:
90
+ return self._state_movements
91
+
92
+ def get_internal_channel_size(self, channel_name):
93
+ is_type_registered = self._internal_channel_type_store.is_valid_name_or_prefix(
94
+ channel_name
95
+ )
96
+
97
+ if is_type_registered is False:
98
+ raise WorkflowDefinitionError(
99
+ f"InternalChannel channel_name is not defined {channel_name}"
100
+ )
101
+
102
+ if (
103
+ self._internal_channel_infos is not None
104
+ and channel_name in self._internal_channel_infos
105
+ ):
106
+ server_channel_size = self._internal_channel_infos[channel_name].size
107
+ else:
108
+ server_channel_size = 0
109
+
110
+ if channel_name in self._to_publish_internal_channel:
111
+ buffer_channel_size = len(self._to_publish_internal_channel[channel_name])
112
+ else:
113
+ buffer_channel_size = 0
114
+
115
+ return server_channel_size + buffer_channel_size
116
+
117
+ def get_signal_channel_size(self, channel_name):
118
+ registered_type = self._signal_channel_type_store.get(channel_name)
119
+
120
+ if registered_type is None:
121
+ for name, t in self._signal_channel_type_store.items():
122
+ if channel_name.startswith(name):
123
+ registered_type = t
124
+
125
+ if registered_type is None:
126
+ raise WorkflowDefinitionError(
127
+ f"SignalChannel channel_name is not defined {channel_name}"
128
+ )
129
+
130
+ if (
131
+ self._signal_channel_infos is not None
132
+ and channel_name in self._signal_channel_infos
133
+ ):
134
+ return self._signal_channel_infos[channel_name].size
135
+ else:
136
+ return 0