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/__init__.py ADDED
@@ -0,0 +1,146 @@
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 dex.attribute import (
12
+ Attribute,
13
+ AttributeIndex,
14
+ AttributeLock,
15
+ AttributeMap,
16
+ IndexType,
17
+ )
18
+ from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
19
+ from dex.channel import Channel, ChannelMap
20
+ from dex.client import Client
21
+ from dex.client_options import ClientOptions
22
+ from dex.codec import (
23
+ BOOL,
24
+ BYTES,
25
+ DOUBLE,
26
+ INT64,
27
+ STRING,
28
+ Codec,
29
+ CodecRegistry,
30
+ JsonCodec,
31
+ Value,
32
+ WireKind,
33
+ )
34
+ from dex.condition import ConditionCombination
35
+ from dex.context import Context
36
+ from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
37
+ from dex.flow_config import ActiveStepSearchMode, FlowConfig
38
+ from dex.flow_info import FlowInfo, FlowStatus, HealthInfo, SearchFlowEntry
39
+ from dex.flow_options import (
40
+ IdReusePolicy,
41
+ ResetFlowOptions,
42
+ ResetType,
43
+ StartFlowOptions,
44
+ StopFlowOptions,
45
+ StopType,
46
+ )
47
+ from dex.runtime_errors import (
48
+ DexException,
49
+ ErrorSubStatus,
50
+ FlowErrorType,
51
+ FlowUncompletedError,
52
+ LongPollTimeoutError,
53
+ )
54
+ from dex.step import (
55
+ RetryPolicy,
56
+ Step,
57
+ StepDecision,
58
+ StepDurability,
59
+ StepList,
60
+ StepMovement,
61
+ StepOptions,
62
+ WaitForFailurePolicy,
63
+ dead_end,
64
+ force_complete,
65
+ force_complete_when_channels_empty,
66
+ force_fail,
67
+ go_to,
68
+ go_to_multi,
69
+ graceful_complete,
70
+ )
71
+ from dex.step_execution import StepExecutionId, TimerId
72
+ from dex.timer import Timer
73
+ from dex.wait import Wait
74
+ from dex.worker import Worker
75
+ from dex.worker_options import WorkerOptions, WorkerTarget
76
+
77
+ __all__ = [
78
+ "BOOL",
79
+ "BYTES",
80
+ "DOUBLE",
81
+ "INT64",
82
+ "STRING",
83
+ "ActiveStepSearchMode",
84
+ "Attribute",
85
+ "AttributeIndex",
86
+ "AttributeLock",
87
+ "AttributeMap",
88
+ "BlobCache",
89
+ "BlobCacheConfig",
90
+ "Channel",
91
+ "ChannelMap",
92
+ "Client",
93
+ "ClientOptions",
94
+ "Codec",
95
+ "CodecRegistry",
96
+ "ConditionCombination",
97
+ "Context",
98
+ "DexException",
99
+ "ErrorSubStatus",
100
+ "Flow",
101
+ "FlowConfig",
102
+ "FlowErrorType",
103
+ "FlowInfo",
104
+ "FlowStatus",
105
+ "FlowUncompletedError",
106
+ "HealthInfo",
107
+ "IdReusePolicy",
108
+ "IndexType",
109
+ "JsonCodec",
110
+ "LongPollTimeoutError",
111
+ "PersistenceSchema",
112
+ "RPCResult",
113
+ "Registry",
114
+ "ResetFlowOptions",
115
+ "ResetType",
116
+ "RetryPolicy",
117
+ "SearchFlowEntry",
118
+ "StartFlowOptions",
119
+ "StepExecutionId",
120
+ "StepDecision",
121
+ "Step",
122
+ "StepList",
123
+ "StepDurability",
124
+ "StepMovement",
125
+ "StepOptions",
126
+ "StopFlowOptions",
127
+ "StopType",
128
+ "Timer",
129
+ "TimerId",
130
+ "Value",
131
+ "Wait",
132
+ "WaitForFailurePolicy",
133
+ "WireKind",
134
+ "Worker",
135
+ "WorkerOptions",
136
+ "WorkerTarget",
137
+ "dead_end",
138
+ "force_complete",
139
+ "force_complete_when_channels_empty",
140
+ "force_fail",
141
+ "graceful_complete",
142
+ "go_to",
143
+ "go_to_multi",
144
+ "open_blob_cache",
145
+ "rpc",
146
+ ]
dex/_grpc_errors.py ADDED
@@ -0,0 +1,73 @@
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
+ from typing import cast
12
+
13
+ import grpc
14
+ from google.protobuf import any_pb2
15
+ from google.rpc import status_pb2
16
+ from grpc_status import rpc_status
17
+
18
+ from dex.dexpb import dex_pb2 as pb
19
+ from dex.runtime_errors import DexException, ErrorSubStatus
20
+
21
+
22
+ def translate_rpc_error(error: grpc.RpcError) -> DexException:
23
+ details: pb.ErrorResponse | None = None
24
+ status = rpc_status.from_call(cast(grpc.Call, error))
25
+ if status is not None:
26
+ for packed in status.details:
27
+ candidate = pb.ErrorResponse()
28
+ if packed.Is(candidate.DESCRIPTOR):
29
+ packed.Unpack(candidate)
30
+ details = candidate
31
+ break
32
+ detail = (
33
+ details.detail if details is not None and details.detail else error.details()
34
+ )
35
+ return DexException(
36
+ error.code(),
37
+ _map_sub_status(details.sub_status) if details is not None else None,
38
+ detail,
39
+ details.original_worker_error_type if details is not None else "",
40
+ details.original_worker_error_detail if details is not None else "",
41
+ )
42
+
43
+
44
+ def abort_worker_error(
45
+ context: grpc.ServicerContext,
46
+ error: BaseException,
47
+ ) -> None:
48
+ message = str(error) or type(error).__name__
49
+ worker_error = pb.WorkerErrorResponse(
50
+ detail=message,
51
+ error_type=f"{type(error).__module__}.{type(error).__qualname__}",
52
+ )
53
+ packed = any_pb2.Any()
54
+ packed.Pack(worker_error)
55
+ status = status_pb2.Status(
56
+ code=grpc.StatusCode.UNKNOWN.value[0],
57
+ message=message,
58
+ details=[packed],
59
+ )
60
+ context.abort_with_status(rpc_status.to_status(status))
61
+
62
+
63
+ def _map_sub_status(value: int) -> ErrorSubStatus | None:
64
+ statuses: dict[int, ErrorSubStatus] = {
65
+ int(pb.ERROR_SUB_STATUS_UNCATEGORIZED): ErrorSubStatus.UNCATEGORIZED,
66
+ int(
67
+ pb.ERROR_SUB_STATUS_FLOW_ALREADY_STARTED
68
+ ): ErrorSubStatus.FLOW_ALREADY_STARTED,
69
+ int(pb.ERROR_SUB_STATUS_FLOW_NOT_EXISTS): ErrorSubStatus.FLOW_NOT_EXISTS,
70
+ int(pb.ERROR_SUB_STATUS_WORKER_API_ERROR): ErrorSubStatus.WORKER_API_ERROR,
71
+ int(pb.ERROR_SUB_STATUS_LONG_POLL_TIME_OUT): ErrorSubStatus.LONG_POLL_TIMEOUT,
72
+ }
73
+ return statuses.get(value)
@@ -0,0 +1,269 @@
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
+ from enum import Enum
12
+ from typing import Any, Sequence, TypeVar, cast
13
+
14
+ from dex._utils import require_name
15
+ from dex._value_mapper import ValueMapper
16
+ from dex.attribute import Attribute, AttributeMap
17
+ from dex.channel import Channel, ChannelMap
18
+ from dex.codec import Codec
19
+ from dex.dexpb import dex_pb2 as pb
20
+ from dex.flow import Registry, _RegisteredFlow
21
+
22
+ ValueT = TypeVar("ValueT")
23
+ _Definition = Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any]
24
+
25
+
26
+ class InvocationMethod(Enum):
27
+ WAIT_FOR = "wait_for"
28
+ EXECUTE = "execute"
29
+ RPC = "rpc"
30
+
31
+
32
+ class InvocationContext:
33
+ def __init__(
34
+ self,
35
+ method: InvocationMethod,
36
+ flow: _RegisteredFlow,
37
+ metadata: pb.Context,
38
+ values: ValueMapper,
39
+ attributes: Sequence[pb.KV],
40
+ locals: Sequence[pb.KV] = (),
41
+ condition_results: pb.ConditionResults | None = None,
42
+ channel_infos: dict[str, pb.ChannelInfo] | None = None,
43
+ ) -> None:
44
+ self._method = method
45
+ self._flow = flow
46
+ self._metadata = metadata
47
+ self._values = values
48
+ self._attributes = self._map_values("Attribute", attributes)
49
+ self._locals = self._map_values("step-execution local", locals)
50
+ self._condition_results = condition_results
51
+ self._channel_infos = dict(channel_infos or {})
52
+ self.attribute_writes: dict[str, pb.AttributeWrite] = {}
53
+ self.local_writes: dict[str, pb.KV] = {}
54
+ self.events: list[pb.KV] = []
55
+ self.publications: list[pb.ChannelMessage] = []
56
+ self._event_names: set[str] = set()
57
+
58
+ @property
59
+ def flow_id(self) -> str:
60
+ return self._metadata.flow_id
61
+
62
+ @property
63
+ def run_id(self) -> str:
64
+ return self._metadata.run_id
65
+
66
+ @property
67
+ def step_execution_id(self) -> str:
68
+ return self._metadata.step_execution_id
69
+
70
+ @property
71
+ def from_step_execution_id(self) -> str:
72
+ return self._metadata.from_step_execution_id
73
+
74
+ @property
75
+ def attempt(self) -> int:
76
+ return self._metadata.attempt
77
+
78
+ def has_timer_fired(self, index: int | None = None) -> bool:
79
+ if self._condition_results is None:
80
+ return False
81
+ results = self._condition_results.timer_results
82
+ if index is None:
83
+ return any(
84
+ result.condition_status == pb.CONDITION_STATUS_COMPLETED
85
+ for result in results
86
+ )
87
+ return (
88
+ 0 <= index < len(results)
89
+ and results[index].condition_status == pb.CONDITION_STATUS_COMPLETED
90
+ )
91
+
92
+ def wait_for_method_failed(self) -> bool:
93
+ return bool(
94
+ self._condition_results is not None
95
+ and self._condition_results.wait_for_failed
96
+ )
97
+
98
+ def set_step_execution_local(self, key: str, value: object) -> None:
99
+ require_name(key)
100
+ self.local_writes[key] = pb.KV(
101
+ key=key,
102
+ value=self._values.encode_dynamic(value),
103
+ )
104
+
105
+ def get_step_execution_local(
106
+ self,
107
+ key: str,
108
+ value_type: type[ValueT],
109
+ ) -> ValueT | None:
110
+ require_name(key)
111
+ entry = self.local_writes.get(key)
112
+ value = entry.value if entry is not None else self._locals.get(key)
113
+ if value is None:
114
+ return None
115
+ return cast(
116
+ ValueT,
117
+ self._values.decode(value, self._flow_codec(value_type)),
118
+ )
119
+
120
+ def record_event(self, name: str, value: object) -> None:
121
+ require_name(name)
122
+ if name in self._event_names:
123
+ raise ValueError(f"event was already recorded: {name}")
124
+ self._event_names.add(name)
125
+ self.events.append(pb.KV(key=name, value=self._values.encode_dynamic(value)))
126
+
127
+ def _get_attribute(
128
+ self,
129
+ definition: Attribute[ValueT] | AttributeMap[ValueT],
130
+ instance: str | None,
131
+ ) -> ValueT:
132
+ self._require_registered(definition)
133
+ key = self._physical_name(definition, instance)
134
+ write = self.attribute_writes.get(key)
135
+ if write is not None:
136
+ if write.value.WhichOneof("kind") == "null_value":
137
+ return cast(ValueT, self._default_value(definition.value_type))
138
+ value: pb.Value | None = write.value
139
+ else:
140
+ value = self._attributes.get(key)
141
+ if value is None:
142
+ return cast(ValueT, self._default_value(definition.value_type))
143
+ codec = self._flow_codec(definition.value_type)
144
+ return cast(ValueT, self._values.decode(value, codec))
145
+
146
+ def _set_attribute(
147
+ self,
148
+ definition: Attribute[ValueT] | AttributeMap[ValueT],
149
+ instance: str | None,
150
+ value: ValueT,
151
+ ) -> None:
152
+ self._require_registered(definition)
153
+ key = self._physical_name(definition, instance)
154
+ write = pb.AttributeWrite(
155
+ key=key,
156
+ value=self._values.encode(value, self._flow_codec(definition.value_type)),
157
+ )
158
+ index = self._values.index_config(
159
+ definition.index,
160
+ isinstance(definition, AttributeMap),
161
+ )
162
+ if index is not None:
163
+ write.index_config.CopyFrom(index)
164
+ self.attribute_writes[key] = write
165
+
166
+ def _delete_attribute(
167
+ self,
168
+ definition: Attribute[object] | AttributeMap[object],
169
+ instance: str | None,
170
+ ) -> None:
171
+ self._require_registered(definition)
172
+ key = self._physical_name(definition, instance)
173
+ write = pb.AttributeWrite(key=key, value=self._values.deletion())
174
+ index = self._values.index_config(
175
+ definition.index,
176
+ isinstance(definition, AttributeMap),
177
+ )
178
+ if index is not None:
179
+ write.index_config.CopyFrom(index)
180
+ self.attribute_writes[key] = write
181
+
182
+ def _publish_channel(
183
+ self,
184
+ definition: Channel[ValueT] | ChannelMap[ValueT],
185
+ instance: str | None,
186
+ value: ValueT,
187
+ ) -> None:
188
+ self._require_registered(definition)
189
+ name = self._physical_name(definition, instance)
190
+ self.publications.append(
191
+ pb.ChannelMessage(
192
+ channel_name=name,
193
+ value=self._values.encode(
194
+ value,
195
+ self._flow_codec(definition.value_type),
196
+ ),
197
+ )
198
+ )
199
+ if self._method is InvocationMethod.RPC:
200
+ current = self._channel_infos.get(name)
201
+ self._channel_infos[name] = pb.ChannelInfo(
202
+ size=(current.size if current is not None else 0) + 1
203
+ )
204
+
205
+ def _channel_size(
206
+ self,
207
+ definition: Channel[object] | ChannelMap[object],
208
+ instance: str | None,
209
+ ) -> int:
210
+ self._require_registered(definition)
211
+ info = self._channel_infos.get(self._physical_name(definition, instance))
212
+ return info.size if info is not None else 0
213
+
214
+ def _channel_results(
215
+ self,
216
+ definition: Channel[ValueT] | ChannelMap[ValueT],
217
+ instance: str | None,
218
+ ) -> Sequence[ValueT]:
219
+ self._require_registered(definition)
220
+ if self._condition_results is None:
221
+ return ()
222
+ name = self._physical_name(definition, instance)
223
+ codec = self._flow_codec(definition.value_type)
224
+ return tuple(
225
+ cast(ValueT, self._values.decode(value, codec))
226
+ for result in self._condition_results.channel_results
227
+ if result.channel_name == name
228
+ and result.condition_status == pb.CONDITION_STATUS_COMPLETED
229
+ for value in result.values
230
+ )
231
+
232
+ def _flow_codec(self, value_type: object) -> Codec[Any]:
233
+ return self._values.codec(value_type)
234
+
235
+ def _require_registered(self, definition: _Definition) -> None:
236
+ registered = self._flow.persistence.get(definition.name)
237
+ if registered is not definition:
238
+ raise ValueError(
239
+ f"persistence definition does not belong to Flow: {definition.name}"
240
+ )
241
+
242
+ @staticmethod
243
+ def _physical_name(definition: _Definition, instance: str | None) -> str:
244
+ if isinstance(definition, (AttributeMap, ChannelMap)):
245
+ if instance is None:
246
+ raise ValueError("dynamic definition requires an instance")
247
+ return Registry.physical_name(definition.name, instance)
248
+ if instance is not None:
249
+ raise ValueError("static definition cannot use an instance")
250
+ return definition.name
251
+
252
+ @staticmethod
253
+ def _map_values(kind: str, entries: Sequence[pb.KV]) -> dict[str, pb.Value]:
254
+ values: dict[str, pb.Value] = {}
255
+ for entry in entries:
256
+ if not entry.key or not entry.HasField("value") or entry.key in values:
257
+ raise ValueError(f"invalid or duplicate {kind}")
258
+ values[entry.key] = entry.value
259
+ return values
260
+
261
+ @staticmethod
262
+ def _default_value(value_type: object) -> object | None:
263
+ if value_type is bool:
264
+ return False
265
+ if value_type is int:
266
+ return 0
267
+ if value_type is float:
268
+ return 0.0
269
+ return None
dex/_native.pyd ADDED
Binary file
dex/_native.pyi ADDED
@@ -0,0 +1,20 @@
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
+ class NativeBlobCache:
10
+ def __init__(
11
+ self,
12
+ directory: str,
13
+ max_bytes: int,
14
+ frequency_counters: int,
15
+ ) -> None: ...
16
+ def get(self, blob_id: str) -> bytes | None: ...
17
+ def put(self, blob_id: str, payload: bytes) -> bool: ...
18
+ def delete(self, blob_id: str) -> None: ...
19
+ def delete_all(self) -> None: ...
20
+ def close(self) -> None: ...
dex/_utils.py ADDED
@@ -0,0 +1,18 @@
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
+
10
+ def require_name(name: str) -> str:
11
+ if not name.strip():
12
+ raise ValueError("durable name is required")
13
+ return name
14
+
15
+
16
+ def validate_condition_id(condition_id: str | None) -> None:
17
+ if condition_id is not None and not condition_id:
18
+ raise ValueError("condition ID must not be empty")