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/_value_hydrator.py ADDED
@@ -0,0 +1,223 @@
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 logging
12
+ from dataclasses import dataclass, field
13
+
14
+ from dex.blob_cache import BlobCache
15
+ from dex.dexpb import dex_pb2 as pb
16
+ from dex.dexpb import dex_pb2_grpc
17
+
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class _PendingBlob:
23
+ blob_id: str
24
+ is_object: bool
25
+ request: pb.Value
26
+ indexes: list[int] = field(default_factory=list)
27
+ hydrated: pb.Value | None = None
28
+
29
+
30
+ class ValueHydrator:
31
+ def __init__(
32
+ self,
33
+ service: dex_pb2_grpc.FlowServiceStub,
34
+ cache: BlobCache,
35
+ ) -> None:
36
+ self._service = service
37
+ self._cache = cache
38
+
39
+ def hydrate(self, value: pb.Value) -> pb.Value:
40
+ return self.hydrate_all([value])[0]
41
+
42
+ def hydrate_all(self, values: list[pb.Value]) -> list[pb.Value]:
43
+ hydrated = list(values)
44
+ pending: dict[tuple[str, bool], _PendingBlob] = {}
45
+ for index, value in enumerate(values):
46
+ key = self._blob_key(value)
47
+ if key is None:
48
+ self._validate_concrete(value)
49
+ continue
50
+ blob = pending.get(key)
51
+ if blob is None:
52
+ blob = _PendingBlob(key[0], key[1], value)
53
+ pending[key] = blob
54
+ blob.indexes.append(index)
55
+
56
+ misses: list[_PendingBlob] = []
57
+ for blob in pending.values():
58
+ concrete = self._read_cache(blob)
59
+ if concrete is None:
60
+ misses.append(blob)
61
+ else:
62
+ blob.hydrated = concrete
63
+ self._load_misses(misses)
64
+
65
+ for blob in pending.values():
66
+ if blob.hydrated is None:
67
+ raise RuntimeError(f"blob was not hydrated: {blob.blob_id}")
68
+ for index in blob.indexes:
69
+ hydrated[index] = blob.hydrated
70
+ return hydrated
71
+
72
+ def wait_for_request(
73
+ self,
74
+ request: pb.InvokeWaitForMethodRequest,
75
+ ) -> pb.InvokeWaitForMethodRequest:
76
+ result = pb.InvokeWaitForMethodRequest()
77
+ result.CopyFrom(request)
78
+ values = [request.step_input, *(entry.value for entry in request.attributes)]
79
+ hydrated = self.hydrate_all(values)
80
+ result.step_input.CopyFrom(hydrated[0])
81
+ for entry, value in zip(result.attributes, hydrated[1:]):
82
+ entry.value.CopyFrom(value)
83
+ return result
84
+
85
+ def execute_request(
86
+ self,
87
+ request: pb.InvokeExecuteMethodRequest,
88
+ ) -> pb.InvokeExecuteMethodRequest:
89
+ result = pb.InvokeExecuteMethodRequest()
90
+ result.CopyFrom(request)
91
+ values = [request.step_input]
92
+ values.extend(entry.value for entry in request.attributes)
93
+ values.extend(entry.value for entry in request.step_exe_locals)
94
+ for channel_result in request.condition_results.channel_results:
95
+ values.extend(channel_result.values)
96
+ hydrated = iter(self.hydrate_all(values))
97
+ result.step_input.CopyFrom(next(hydrated))
98
+ for entry in result.attributes:
99
+ entry.value.CopyFrom(next(hydrated))
100
+ for entry in result.step_exe_locals:
101
+ entry.value.CopyFrom(next(hydrated))
102
+ for channel_result in result.condition_results.channel_results:
103
+ for value in channel_result.values:
104
+ value.CopyFrom(next(hydrated))
105
+ return result
106
+
107
+ def rpc_request(
108
+ self,
109
+ request: pb.InvokeWorkerRPCRequest,
110
+ ) -> pb.InvokeWorkerRPCRequest:
111
+ result = pb.InvokeWorkerRPCRequest()
112
+ result.CopyFrom(request)
113
+ values = [request.input, *(entry.value for entry in request.attributes)]
114
+ hydrated = self.hydrate_all(values)
115
+ result.input.CopyFrom(hydrated[0])
116
+ for entry, value in zip(result.attributes, hydrated[1:]):
117
+ entry.value.CopyFrom(value)
118
+ return result
119
+
120
+ def step_outputs(
121
+ self,
122
+ outputs: list[pb.StepCompletionOutput],
123
+ ) -> list[pb.StepCompletionOutput]:
124
+ values = [output.completed_step_output for output in outputs]
125
+ hydrated = self.hydrate_all(values)
126
+ results: list[pb.StepCompletionOutput] = []
127
+ for output, value in zip(outputs, hydrated):
128
+ result = pb.StepCompletionOutput()
129
+ result.CopyFrom(output)
130
+ result.completed_step_output.CopyFrom(value)
131
+ results.append(result)
132
+ return results
133
+
134
+ def _load_misses(self, misses: list[_PendingBlob]) -> None:
135
+ if not misses:
136
+ return
137
+ response = self._service.LoadBlobs(
138
+ pb.LoadBlobsRequest(values=[miss.request for miss in misses])
139
+ )
140
+ for miss in misses:
141
+ concrete = response.values.get(miss.blob_id)
142
+ if concrete is None:
143
+ raise RuntimeError(f"LoadBlobs omitted blob {miss.blob_id}")
144
+ self._validate_hydrated(miss, concrete)
145
+ miss.hydrated = concrete
146
+ self._write_cache(miss, concrete)
147
+
148
+ def _read_cache(self, blob: _PendingBlob) -> pb.Value | None:
149
+ try:
150
+ payload = self._cache.get(blob.blob_id)
151
+ if payload is None:
152
+ return None
153
+ if blob.is_object:
154
+ concrete = pb.Value(obj_value=pb.EncodedObject.FromString(payload))
155
+ else:
156
+ concrete = pb.Value(string_value=payload.decode("utf-8"))
157
+ self._validate_hydrated(blob, concrete)
158
+ return concrete
159
+ except Exception:
160
+ _LOGGER.warning("cannot read cached blob %s", blob.blob_id, exc_info=True)
161
+ try:
162
+ self._cache.delete(blob.blob_id)
163
+ except Exception:
164
+ _LOGGER.warning(
165
+ "cannot delete cached blob %s",
166
+ blob.blob_id,
167
+ exc_info=True,
168
+ )
169
+ return None
170
+
171
+ def _write_cache(self, blob: _PendingBlob, concrete: pb.Value) -> None:
172
+ try:
173
+ payload = (
174
+ concrete.obj_value.SerializeToString()
175
+ if blob.is_object
176
+ else concrete.string_value.encode("utf-8")
177
+ )
178
+ self._cache.put(blob.blob_id, payload)
179
+ except Exception:
180
+ _LOGGER.warning("cannot cache blob %s", blob.blob_id, exc_info=True)
181
+
182
+ @staticmethod
183
+ def _blob_key(value: pb.Value) -> tuple[str, bool] | None:
184
+ kind = value.WhichOneof("kind")
185
+ if kind == "internal_blob_id_for_string_value":
186
+ blob_id = value.internal_blob_id_for_string_value
187
+ if not blob_id:
188
+ raise ValueError("blob ID is required")
189
+ return blob_id, False
190
+ if kind == "internal_blob_id_for_obj_value":
191
+ blob_id = value.internal_blob_id_for_obj_value
192
+ if not blob_id:
193
+ raise ValueError("blob ID is required")
194
+ return blob_id, True
195
+ return None
196
+
197
+ @staticmethod
198
+ def _validate_hydrated(blob: _PendingBlob, value: pb.Value) -> None:
199
+ expected = "obj_value" if blob.is_object else "string_value"
200
+ if value.WhichOneof("kind") != expected:
201
+ raise RuntimeError(
202
+ f"blob {blob.blob_id} hydrated to {value.WhichOneof('kind')}"
203
+ )
204
+ ValueHydrator._validate_concrete(value)
205
+
206
+ @staticmethod
207
+ def _validate_concrete(value: pb.Value) -> None:
208
+ kind = value.WhichOneof("kind")
209
+ if kind in ("string_value", "int_value", "bool_value", "null_value"):
210
+ return
211
+ if kind == "double_value":
212
+ import math
213
+
214
+ if not math.isfinite(value.double_value):
215
+ raise ValueError("non-finite numbers are unsupported")
216
+ return
217
+ if kind == "obj_value":
218
+ if value.obj_value.encoding not in ("json", "rawbytes"):
219
+ raise ValueError(
220
+ f"unsupported object encoding {value.obj_value.encoding}"
221
+ )
222
+ return
223
+ raise ValueError("Value has no concrete kind")
dex/_value_mapper.py ADDED
@@ -0,0 +1,174 @@
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
+ from dataclasses import asdict, is_dataclass
13
+ from datetime import datetime
14
+ from enum import Enum
15
+ from typing import Any, cast
16
+
17
+ from google.protobuf import struct_pb2
18
+
19
+ from dex.attribute import AttributeIndex, IndexType
20
+ from dex.codec import Codec, CodecRegistry, Value, WireKind
21
+ from dex.dexpb import dex_pb2 as pb
22
+
23
+ _JSON_ENCODING = "json"
24
+ _RAW_BYTES_ENCODING = "rawbytes"
25
+
26
+
27
+ class ValueMapper:
28
+ def __init__(self, codecs: CodecRegistry) -> None:
29
+ self._codecs = codecs
30
+
31
+ def codec(self, value_type: object) -> Codec[Any]:
32
+ return self._codecs.resolve(value_type)
33
+
34
+ def encode(self, value: Any, codec: Codec[Any]) -> pb.Value:
35
+ if value is None:
36
+ return pb.Value(
37
+ obj_value=pb.EncodedObject(
38
+ encoding=_JSON_ENCODING,
39
+ payload=b"null",
40
+ )
41
+ )
42
+ logical = codec.encode(value)
43
+ if logical.kind is WireKind.STRING:
44
+ return pb.Value(string_value=self._require(logical.data, str))
45
+ if logical.kind is WireKind.BOOL:
46
+ return pb.Value(bool_value=self._require_exact(logical.data, bool))
47
+ if logical.kind is WireKind.INT64:
48
+ return pb.Value(int_value=self._require_exact(logical.data, int))
49
+ if logical.kind is WireKind.DOUBLE:
50
+ return pb.Value(double_value=self._require_exact(logical.data, float))
51
+ if logical.kind is WireKind.BYTES:
52
+ return pb.Value(
53
+ obj_value=pb.EncodedObject(
54
+ encoding=_RAW_BYTES_ENCODING,
55
+ payload=self._require(logical.data, bytes),
56
+ )
57
+ )
58
+ if logical.kind is WireKind.JSON:
59
+ payload = self._require(logical.data, str).encode("utf-8")
60
+ return pb.Value(
61
+ obj_value=pb.EncodedObject(
62
+ encoding=_JSON_ENCODING,
63
+ payload=payload,
64
+ )
65
+ )
66
+ raise TypeError(f"unsupported wire kind {logical.kind}")
67
+
68
+ def encode_dynamic(self, value: Any) -> pb.Value:
69
+ if value is None:
70
+ return self.encode(value, self._codecs.resolve(type(None)))
71
+ value_type = type(value)
72
+ try:
73
+ codec = self._codecs.resolve(value_type)
74
+ except TypeError:
75
+ if isinstance(value, (list, tuple, dict)):
76
+ return pb.Value(
77
+ obj_value=pb.EncodedObject(
78
+ encoding=_JSON_ENCODING,
79
+ payload=self._encode_json_collection(value),
80
+ )
81
+ )
82
+ raise
83
+ return self.encode(value, codec)
84
+
85
+ def decode(self, value: pb.Value, codec: Codec[Any]) -> Any:
86
+ kind = value.WhichOneof("kind")
87
+ expected = codec.wire_kind
88
+ if kind == "string_value" and expected is WireKind.STRING:
89
+ return codec.decode(Value(WireKind.STRING, value.string_value))
90
+ if kind == "bool_value" and expected is WireKind.BOOL:
91
+ return codec.decode(Value(WireKind.BOOL, value.bool_value))
92
+ if kind == "int_value" and expected is WireKind.INT64:
93
+ return codec.decode(Value(WireKind.INT64, value.int_value))
94
+ if kind == "double_value" and expected is WireKind.DOUBLE:
95
+ return codec.decode(Value(WireKind.DOUBLE, value.double_value))
96
+ if kind == "obj_value" and expected is WireKind.BYTES:
97
+ self._require_encoding(value.obj_value, _RAW_BYTES_ENCODING)
98
+ return codec.decode(Value(WireKind.BYTES, value.obj_value.payload))
99
+ if kind == "obj_value" and expected is WireKind.JSON:
100
+ self._require_encoding(value.obj_value, _JSON_ENCODING)
101
+ payload = value.obj_value.payload.decode("utf-8")
102
+ return codec.decode(Value(WireKind.JSON, payload))
103
+ if kind == "obj_value" and value.obj_value.encoding == _JSON_ENCODING:
104
+ if value.obj_value.payload == b"null":
105
+ return None
106
+ if kind in (
107
+ "internal_blob_id_for_string_value",
108
+ "internal_blob_id_for_obj_value",
109
+ ):
110
+ raise ValueError("blob-backed Value was not hydrated")
111
+ if kind == "null_value":
112
+ raise ValueError("attribute deletion marker cannot be decoded")
113
+ raise TypeError(f"cannot decode {kind or 'empty Value'} as {codec.type_name}")
114
+
115
+ @staticmethod
116
+ def deletion() -> pb.Value:
117
+ return pb.Value(null_value=cast(struct_pb2.NullValue, struct_pb2.NULL_VALUE))
118
+
119
+ @staticmethod
120
+ def index_config(
121
+ index: AttributeIndex | None,
122
+ dynamic: bool,
123
+ ) -> pb.IndexConfig | None:
124
+ if index is None:
125
+ return None
126
+ mapped_types = {
127
+ IndexType.KEYWORD: pb.INDEX_TYPE_KEYWORD,
128
+ IndexType.FULL_TEXT: pb.INDEX_TYPE_TEXT,
129
+ IndexType.KEYWORD_ARRAY: pb.INDEX_TYPE_KEYWORD_ARRAY,
130
+ IndexType.INT: pb.INDEX_TYPE_INT,
131
+ IndexType.DOUBLE: pb.INDEX_TYPE_DOUBLE,
132
+ IndexType.BOOL: pb.INDEX_TYPE_BOOL,
133
+ IndexType.DATETIME: pb.INDEX_TYPE_DATETIME,
134
+ }
135
+ config = pb.IndexConfig(enable=True, type=mapped_types[index.type])
136
+ if index.index_key or dynamic:
137
+ config.index_key = index.index_key
138
+ return config
139
+
140
+ @staticmethod
141
+ def _require(value: object, expected: type[Any]) -> Any:
142
+ if not isinstance(value, expected):
143
+ raise TypeError(f"expected {expected.__name__}")
144
+ return value
145
+
146
+ @staticmethod
147
+ def _require_exact(value: object, expected: type[Any]) -> Any:
148
+ if type(value) is not expected:
149
+ raise TypeError(f"expected {expected.__name__}")
150
+ return value
151
+
152
+ @staticmethod
153
+ def _require_encoding(value: pb.EncodedObject, expected: str) -> None:
154
+ if value.encoding != expected:
155
+ raise TypeError(f"expected {expected} encoding, got {value.encoding}")
156
+
157
+ @staticmethod
158
+ def _encode_json_collection(value: object) -> bytes:
159
+ def default(item: object) -> object:
160
+ if is_dataclass(item) and not isinstance(item, type):
161
+ return asdict(item)
162
+ if isinstance(item, datetime):
163
+ return item.isoformat()
164
+ if isinstance(item, Enum):
165
+ return item.value
166
+ raise TypeError(f"cannot encode {type(item).__name__} as JSON")
167
+
168
+ return json.dumps(
169
+ value,
170
+ default=default,
171
+ allow_nan=False,
172
+ separators=(",", ":"),
173
+ sort_keys=True,
174
+ ).encode("utf-8")