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