dex-python-sdk 0.1.1__tar.gz → 0.1.3__tar.gz
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_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/PKG-INFO +71 -3
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/README.md +70 -2
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/__init__.py +20 -4
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_async_worker_dispatcher.py +59 -43
- dex_python_sdk-0.1.3/dex/_grpc_errors.py +145 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_invocation_context.py +36 -1
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_value_mapper.py +33 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_worker_dispatcher.py +75 -57
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/async_client.py +453 -30
- dex_python_sdk-0.1.3/dex/async_worker.py +189 -0
- dex_python_sdk-0.1.3/dex/attribute.py +265 -0
- dex_python_sdk-0.1.3/dex/blob_cache.py +145 -0
- dex_python_sdk-0.1.3/dex/channel.py +364 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/client.py +443 -24
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/client_options.py +9 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/codec.py +143 -4
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/condition.py +17 -0
- dex_python_sdk-0.1.3/dex/context.py +179 -0
- dex_python_sdk-0.1.3/dex/dexpb/dex_pb2.py +395 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/dexpb/dex_pb2.pyi +82 -31
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/dexpb/dex_pb2_grpc.py +43 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/flow.py +216 -23
- dex_python_sdk-0.1.3/dex/flow_config.py +59 -0
- dex_python_sdk-0.1.3/dex/flow_info.py +107 -0
- dex_python_sdk-0.1.3/dex/flow_options.py +214 -0
- dex_python_sdk-0.1.3/dex/runtime_errors.py +285 -0
- dex_python_sdk-0.1.3/dex/step.py +427 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/step_execution.py +36 -0
- dex_python_sdk-0.1.3/dex/timer.py +44 -0
- dex_python_sdk-0.1.3/dex/wait.py +108 -0
- dex_python_sdk-0.1.3/dex/worker.py +214 -0
- dex_python_sdk-0.1.3/dex/worker_options.py +52 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/pyproject.toml +1 -1
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/Cargo.lock +140 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/Cargo.toml +5 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/config.rs +31 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/error.rs +11 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/lib.rs +76 -0
- dex_python_sdk-0.1.1/dex/_grpc_errors.py +0 -86
- dex_python_sdk-0.1.1/dex/async_worker.py +0 -106
- dex_python_sdk-0.1.1/dex/attribute.py +0 -87
- dex_python_sdk-0.1.1/dex/blob_cache.py +0 -73
- dex_python_sdk-0.1.1/dex/channel.py +0 -165
- dex_python_sdk-0.1.1/dex/context.py +0 -86
- dex_python_sdk-0.1.1/dex/dexpb/dex_pb2.py +0 -381
- dex_python_sdk-0.1.1/dex/flow_config.py +0 -29
- dex_python_sdk-0.1.1/dex/flow_info.py +0 -57
- dex_python_sdk-0.1.1/dex/flow_options.py +0 -122
- dex_python_sdk-0.1.1/dex/runtime_errors.py +0 -81
- dex_python_sdk-0.1.1/dex/step.py +0 -194
- dex_python_sdk-0.1.1/dex/timer.py +0 -21
- dex_python_sdk-0.1.1/dex/wait.py +0 -49
- dex_python_sdk-0.1.1/dex/worker.py +0 -121
- dex_python_sdk-0.1.1/dex/worker_options.py +0 -22
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_utils.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/dex/py.typed +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -39,7 +39,7 @@ class Run(dex.Step[str]):
|
|
|
39
39
|
def wait_for(
|
|
40
40
|
self, context: dex.Context, input: str
|
|
41
41
|
) -> dex.Wait:
|
|
42
|
-
return dex.Wait.
|
|
42
|
+
return dex.Wait.until(
|
|
43
43
|
dex.Timer.by_duration(timedelta(seconds=1))
|
|
44
44
|
)
|
|
45
45
|
|
|
@@ -76,6 +76,11 @@ explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
|
76
76
|
`PersistenceSchema.of(...)` accepts attributes and channels together and
|
|
77
77
|
partitions them by definition type.
|
|
78
78
|
|
|
79
|
+
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
80
|
+
Dex Server before opening their listener. Existing indexes return immediately;
|
|
81
|
+
failure or the default two-minute deadline aborts startup. An indexed
|
|
82
|
+
`AttributeMap` must provide one fixed `index_key`.
|
|
83
|
+
|
|
79
84
|
Initial attributes retain their value types without a public wrapper class:
|
|
80
85
|
|
|
81
86
|
```python
|
|
@@ -86,6 +91,19 @@ options = (
|
|
|
86
91
|
)
|
|
87
92
|
```
|
|
88
93
|
|
|
94
|
+
Opt in when declaring an Attribute or AttributeMap, and select the Store in
|
|
95
|
+
Flow configuration:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
|
|
99
|
+
config = dex.FlowConfig(attribute_store_name="profiles")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The Store is an asynchronous latest-state projection. Deletion writes SQL
|
|
103
|
+
`NULL`, and projection failures do not roll back Flow Attributes. `None`
|
|
104
|
+
preserves the current target; an explicit empty string disables future
|
|
105
|
+
synchronization while retaining protocol presence.
|
|
106
|
+
|
|
89
107
|
```
|
|
90
108
|
pip install dex-python-sdk==0.1.0
|
|
91
109
|
```
|
|
@@ -117,6 +135,45 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
|
117
135
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
118
136
|
instead of raw Flow, Step, or RPC strings.
|
|
119
137
|
|
|
138
|
+
### Waiting and map inspection
|
|
139
|
+
|
|
140
|
+
`Wait.all_of` and `Wait.any_of` may use unnamed Conditions. Every Condition in
|
|
141
|
+
`Wait.any_combination_of` must have a non-empty user ID; the same Condition
|
|
142
|
+
instance may appear in multiple combinations.
|
|
143
|
+
|
|
144
|
+
Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
|
|
145
|
+
`wait_for_attribute_map_equal`. They target the current run and accept only
|
|
146
|
+
string, bool, int, or float wire values. JSON objects, bytes, and null fail
|
|
147
|
+
before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
|
|
148
|
+
buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
|
|
149
|
+
include buffered publishes, and omit empty instances. Keys are decoded and
|
|
150
|
+
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
151
|
+
|
|
152
|
+
### Errors
|
|
153
|
+
|
|
154
|
+
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
155
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
|
|
156
|
+
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
157
|
+
waits, config updates, and continue-as-new triggers raise
|
|
158
|
+
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
try:
|
|
162
|
+
client.publish(flow_id, orders.approved, order_id)
|
|
163
|
+
except dex.FlowNotActiveError:
|
|
164
|
+
# The Flow is missing or already closed.
|
|
165
|
+
pass
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
|
|
169
|
+
raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
|
|
170
|
+
`RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
|
|
171
|
+
`code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
|
|
172
|
+
exception through Python exception chaining. Worker failures also expose
|
|
173
|
+
`worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
|
|
174
|
+
serialization, and invalid handler returns use `FlowDefinitionError`,
|
|
175
|
+
`ValueMappingError`, and `InvalidStepResultError`.
|
|
176
|
+
|
|
120
177
|
### Sync vs asyncio
|
|
121
178
|
|
|
122
179
|
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
@@ -209,7 +266,18 @@ make -C ../protos proto-python
|
|
|
209
266
|
Checked-in Python stubs land in `dex/dexpb/`.
|
|
210
267
|
#### Linting
|
|
211
268
|
|
|
212
|
-
|
|
269
|
+
Validate that every `dex.__all__` class, function, constant, public method,
|
|
270
|
+
argument, return value, dataclass field, enum value, and public instance
|
|
271
|
+
attribute has a Google-style docstring:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
uv run --frozen python scripts/check_public_docs.py
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The checker resolves definitions from the public package export table, so
|
|
278
|
+
private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
|
|
279
|
+
or IDE hover information to read the same documentation. To run all other
|
|
280
|
+
linting for this project:
|
|
213
281
|
|
|
214
282
|
```bash
|
|
215
283
|
uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files
|
|
@@ -23,7 +23,7 @@ class Run(dex.Step[str]):
|
|
|
23
23
|
def wait_for(
|
|
24
24
|
self, context: dex.Context, input: str
|
|
25
25
|
) -> dex.Wait:
|
|
26
|
-
return dex.Wait.
|
|
26
|
+
return dex.Wait.until(
|
|
27
27
|
dex.Timer.by_duration(timedelta(seconds=1))
|
|
28
28
|
)
|
|
29
29
|
|
|
@@ -60,6 +60,11 @@ explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
|
60
60
|
`PersistenceSchema.of(...)` accepts attributes and channels together and
|
|
61
61
|
partitions them by definition type.
|
|
62
62
|
|
|
63
|
+
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
64
|
+
Dex Server before opening their listener. Existing indexes return immediately;
|
|
65
|
+
failure or the default two-minute deadline aborts startup. An indexed
|
|
66
|
+
`AttributeMap` must provide one fixed `index_key`.
|
|
67
|
+
|
|
63
68
|
Initial attributes retain their value types without a public wrapper class:
|
|
64
69
|
|
|
65
70
|
```python
|
|
@@ -70,6 +75,19 @@ options = (
|
|
|
70
75
|
)
|
|
71
76
|
```
|
|
72
77
|
|
|
78
|
+
Opt in when declaring an Attribute or AttributeMap, and select the Store in
|
|
79
|
+
Flow configuration:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
|
|
83
|
+
config = dex.FlowConfig(attribute_store_name="profiles")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The Store is an asynchronous latest-state projection. Deletion writes SQL
|
|
87
|
+
`NULL`, and projection failures do not roll back Flow Attributes. `None`
|
|
88
|
+
preserves the current target; an explicit empty string disables future
|
|
89
|
+
synchronization while retaining protocol presence.
|
|
90
|
+
|
|
73
91
|
```
|
|
74
92
|
pip install dex-python-sdk==0.1.0
|
|
75
93
|
```
|
|
@@ -101,6 +119,45 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
|
101
119
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
102
120
|
instead of raw Flow, Step, or RPC strings.
|
|
103
121
|
|
|
122
|
+
### Waiting and map inspection
|
|
123
|
+
|
|
124
|
+
`Wait.all_of` and `Wait.any_of` may use unnamed Conditions. Every Condition in
|
|
125
|
+
`Wait.any_combination_of` must have a non-empty user ID; the same Condition
|
|
126
|
+
instance may appear in multiple combinations.
|
|
127
|
+
|
|
128
|
+
Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
|
|
129
|
+
`wait_for_attribute_map_equal`. They target the current run and accept only
|
|
130
|
+
string, bool, int, or float wire values. JSON objects, bytes, and null fail
|
|
131
|
+
before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
|
|
132
|
+
buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
|
|
133
|
+
include buffered publishes, and omit empty instances. Keys are decoded and
|
|
134
|
+
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
135
|
+
|
|
136
|
+
### Errors
|
|
137
|
+
|
|
138
|
+
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
139
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
|
|
140
|
+
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
141
|
+
waits, config updates, and continue-as-new triggers raise
|
|
142
|
+
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
try:
|
|
146
|
+
client.publish(flow_id, orders.approved, order_id)
|
|
147
|
+
except dex.FlowNotActiveError:
|
|
148
|
+
# The Flow is missing or already closed.
|
|
149
|
+
pass
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
|
|
153
|
+
raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
|
|
154
|
+
`RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
|
|
155
|
+
`code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
|
|
156
|
+
exception through Python exception chaining. Worker failures also expose
|
|
157
|
+
`worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
|
|
158
|
+
serialization, and invalid handler returns use `FlowDefinitionError`,
|
|
159
|
+
`ValueMappingError`, and `InvalidStepResultError`.
|
|
160
|
+
|
|
104
161
|
### Sync vs asyncio
|
|
105
162
|
|
|
106
163
|
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
@@ -193,7 +250,18 @@ make -C ../protos proto-python
|
|
|
193
250
|
Checked-in Python stubs land in `dex/dexpb/`.
|
|
194
251
|
#### Linting
|
|
195
252
|
|
|
196
|
-
|
|
253
|
+
Validate that every `dex.__all__` class, function, constant, public method,
|
|
254
|
+
argument, return value, dataclass field, enum value, and public instance
|
|
255
|
+
attribute has a Google-style docstring:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
uv run --frozen python scripts/check_public_docs.py
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The checker resolves definitions from the public package export table, so
|
|
262
|
+
private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
|
|
263
|
+
or IDE hover information to read the same documentation. To run all other
|
|
264
|
+
linting for this project:
|
|
197
265
|
|
|
198
266
|
```bash
|
|
199
267
|
uv run --frozen pre-commit run --show-diff-on-failure --color=always --all-files
|
|
@@ -53,11 +53,19 @@ from dex.flow_options import (
|
|
|
53
53
|
StopType,
|
|
54
54
|
)
|
|
55
55
|
from dex.runtime_errors import (
|
|
56
|
-
|
|
56
|
+
DexServiceError,
|
|
57
57
|
ErrorSubStatus,
|
|
58
|
+
FlowAlreadyStartedError,
|
|
59
|
+
FlowDefinitionError,
|
|
58
60
|
FlowErrorType,
|
|
61
|
+
FlowNotActiveError,
|
|
62
|
+
FlowNotFoundError,
|
|
59
63
|
FlowUncompletedError,
|
|
64
|
+
InvalidStepResultError,
|
|
60
65
|
LongPollTimeoutError,
|
|
66
|
+
RpcLockConflictError,
|
|
67
|
+
ValueMappingError,
|
|
68
|
+
WorkerInvocationError,
|
|
61
69
|
)
|
|
62
70
|
from dex.step import (
|
|
63
71
|
RetryPolicy,
|
|
@@ -70,7 +78,7 @@ from dex.step import (
|
|
|
70
78
|
WaitForFailurePolicy,
|
|
71
79
|
dead_end,
|
|
72
80
|
force_complete,
|
|
73
|
-
|
|
81
|
+
force_complete_if_channels_empty,
|
|
74
82
|
force_fail,
|
|
75
83
|
go_to,
|
|
76
84
|
go_to_multi,
|
|
@@ -105,21 +113,27 @@ __all__ = [
|
|
|
105
113
|
"CodecRegistry",
|
|
106
114
|
"ConditionCombination",
|
|
107
115
|
"Context",
|
|
108
|
-
"
|
|
116
|
+
"DexServiceError",
|
|
109
117
|
"ErrorSubStatus",
|
|
110
118
|
"Flow",
|
|
119
|
+
"FlowAlreadyStartedError",
|
|
111
120
|
"FlowConfig",
|
|
121
|
+
"FlowDefinitionError",
|
|
112
122
|
"FlowErrorType",
|
|
113
123
|
"FlowInfo",
|
|
124
|
+
"FlowNotActiveError",
|
|
125
|
+
"FlowNotFoundError",
|
|
114
126
|
"FlowStatus",
|
|
115
127
|
"FlowUncompletedError",
|
|
116
128
|
"HealthInfo",
|
|
117
129
|
"IdReusePolicy",
|
|
130
|
+
"InvalidStepResultError",
|
|
118
131
|
"IndexType",
|
|
119
132
|
"JsonCodec",
|
|
120
133
|
"LongPollTimeoutError",
|
|
121
134
|
"PersistenceSchema",
|
|
122
135
|
"RPCResult",
|
|
136
|
+
"RpcLockConflictError",
|
|
123
137
|
"Registry",
|
|
124
138
|
"ResetFlowOptions",
|
|
125
139
|
"ResetType",
|
|
@@ -139,15 +153,17 @@ __all__ = [
|
|
|
139
153
|
"Timer",
|
|
140
154
|
"TimerId",
|
|
141
155
|
"Value",
|
|
156
|
+
"ValueMappingError",
|
|
142
157
|
"Wait",
|
|
143
158
|
"WaitForFailurePolicy",
|
|
144
159
|
"WireKind",
|
|
145
160
|
"Worker",
|
|
161
|
+
"WorkerInvocationError",
|
|
146
162
|
"WorkerOptions",
|
|
147
163
|
"WorkerTarget",
|
|
148
164
|
"dead_end",
|
|
149
165
|
"force_complete",
|
|
150
|
-
"
|
|
166
|
+
"force_complete_if_channels_empty",
|
|
151
167
|
"force_fail",
|
|
152
168
|
"graceful_complete",
|
|
153
169
|
"go_to",
|
|
@@ -17,6 +17,7 @@ from dex._value_mapper import ValueMapper
|
|
|
17
17
|
from dex._worker_dispatcher import WorkerDispatcher
|
|
18
18
|
from dex.dexpb import dex_pb2 as pb
|
|
19
19
|
from dex.flow import RPCResult, Registry
|
|
20
|
+
from dex.runtime_errors import InvalidStepResultError, ValueMappingError
|
|
20
21
|
from dex.step import StepDecision
|
|
21
22
|
from dex.wait import Wait
|
|
22
23
|
|
|
@@ -30,13 +31,13 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
30
31
|
) -> None:
|
|
31
32
|
self._registry = registry
|
|
32
33
|
self._values = values
|
|
33
|
-
self.
|
|
34
|
+
self._async_hydrator = hydrator
|
|
34
35
|
|
|
35
36
|
async def invoke_wait_for( # type: ignore[override]
|
|
36
37
|
self,
|
|
37
38
|
original: pb.InvokeWaitForMethodRequest,
|
|
38
39
|
) -> pb.InvokeWaitForMethodResponse:
|
|
39
|
-
request = await self.
|
|
40
|
+
request = await self._async_hydrator.wait_for_request(original)
|
|
40
41
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
41
42
|
step = flow.step(request.step_type)
|
|
42
43
|
context = InvocationContext(
|
|
@@ -50,24 +51,29 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
50
51
|
wait = step.step.wait_for(context, input)
|
|
51
52
|
if isawaitable(wait):
|
|
52
53
|
wait = await wait
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
try:
|
|
55
|
+
if not isinstance(wait, Wait):
|
|
56
|
+
raise TypeError("wait_for must return Wait")
|
|
57
|
+
response = pb.InvokeWaitForMethodResponse(
|
|
58
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
59
|
+
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
60
|
+
record_events=context.events,
|
|
61
|
+
publish_to_channel=context.publications,
|
|
62
|
+
)
|
|
63
|
+
waiting = self._map_wait(flow, wait)
|
|
64
|
+
if waiting is not None:
|
|
65
|
+
response.waiting_condition.CopyFrom(waiting)
|
|
66
|
+
return response
|
|
67
|
+
except (TypeError, ValueError) as error:
|
|
68
|
+
raise InvalidStepResultError(
|
|
69
|
+
flow.name, step.name, "wait_for", str(error)
|
|
70
|
+
) from error
|
|
65
71
|
|
|
66
72
|
async def invoke_execute( # type: ignore[override]
|
|
67
73
|
self,
|
|
68
74
|
original: pb.InvokeExecuteMethodRequest,
|
|
69
75
|
) -> pb.InvokeExecuteMethodResponse:
|
|
70
|
-
request = await self.
|
|
76
|
+
request = await self._async_hydrator.execute_request(original)
|
|
71
77
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
72
78
|
step = flow.step(request.step_type)
|
|
73
79
|
condition_results = (
|
|
@@ -86,21 +92,26 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
86
92
|
decision: Any = step.step.execute(context, input)
|
|
87
93
|
if isawaitable(decision):
|
|
88
94
|
decision = await decision
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
try:
|
|
96
|
+
if not isinstance(decision, StepDecision):
|
|
97
|
+
raise TypeError("execute must return StepDecision")
|
|
98
|
+
return pb.InvokeExecuteMethodResponse(
|
|
99
|
+
step_decision=self._map_decision(flow, decision),
|
|
100
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
101
|
+
record_events=context.events,
|
|
102
|
+
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
103
|
+
publish_to_channel=context.publications,
|
|
104
|
+
)
|
|
105
|
+
except (TypeError, ValueError) as error:
|
|
106
|
+
raise InvalidStepResultError(
|
|
107
|
+
flow.name, step.name, "execute", str(error)
|
|
108
|
+
) from error
|
|
98
109
|
|
|
99
110
|
async def invoke_rpc( # type: ignore[override]
|
|
100
111
|
self,
|
|
101
112
|
original: pb.InvokeWorkerRPCRequest,
|
|
102
113
|
) -> pb.InvokeWorkerRPCResponse:
|
|
103
|
-
request = await self.
|
|
114
|
+
request = await self._async_hydrator.rpc_request(original)
|
|
104
115
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
105
116
|
rpc = flow.rpc(request.rpc_name)
|
|
106
117
|
context = InvocationContext(
|
|
@@ -117,23 +128,28 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
117
128
|
returned: Any = rpc.method(*arguments)
|
|
118
129
|
if isawaitable(returned):
|
|
119
130
|
returned = await returned
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if isinstance(returned, RPCResult):
|
|
126
|
-
if rpc.output_codec is None:
|
|
127
|
-
raise TypeError("RPCResult requires an output type")
|
|
128
|
-
response.output.CopyFrom(
|
|
129
|
-
self._values.encode(returned.output, rpc.output_codec)
|
|
131
|
+
try:
|
|
132
|
+
response = pb.InvokeWorkerRPCResponse(
|
|
133
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
134
|
+
record_events=context.events,
|
|
135
|
+
publish_to_channel=context.publications,
|
|
130
136
|
)
|
|
131
|
-
if returned
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
if isinstance(returned, RPCResult):
|
|
138
|
+
if rpc.output_codec is None:
|
|
139
|
+
raise TypeError("RPCResult requires an output type")
|
|
140
|
+
response.output.CopyFrom(
|
|
141
|
+
self._values.encode(returned.output, rpc.output_codec)
|
|
134
142
|
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
if returned.next_steps:
|
|
144
|
+
response.step_decision.next_steps.extend(
|
|
145
|
+
self._map_movements(flow, returned.next_steps)
|
|
146
|
+
)
|
|
147
|
+
elif returned is None and rpc.output_codec is None:
|
|
148
|
+
response.output.CopyFrom(self._values.encode_dynamic(None))
|
|
149
|
+
else:
|
|
150
|
+
raise TypeError("RPC must return RPCResult or None")
|
|
151
|
+
return response
|
|
152
|
+
except ValueMappingError:
|
|
153
|
+
raise
|
|
154
|
+
except (TypeError, ValueError) as error:
|
|
155
|
+
raise InvalidStepResultError(flow.name, None, "rpc", str(error)) from error
|
|
@@ -0,0 +1,145 @@
|
|
|
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 Any, Literal, 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 (
|
|
20
|
+
DexServiceError,
|
|
21
|
+
ErrorSubStatus,
|
|
22
|
+
FlowAlreadyStartedError,
|
|
23
|
+
FlowNotActiveError,
|
|
24
|
+
FlowNotFoundError,
|
|
25
|
+
LongPollTimeoutError,
|
|
26
|
+
RpcLockConflictError,
|
|
27
|
+
WorkerInvocationError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
FlowTargetRequirement = Literal["none", "existing", "active"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def translate_rpc_error(
|
|
34
|
+
error: grpc.RpcError,
|
|
35
|
+
operation: str,
|
|
36
|
+
flow_id: str | None,
|
|
37
|
+
requirement: FlowTargetRequirement,
|
|
38
|
+
) -> DexServiceError:
|
|
39
|
+
details: pb.ErrorResponse | None = None
|
|
40
|
+
try:
|
|
41
|
+
status = rpc_status.from_call(cast(grpc.Call, error))
|
|
42
|
+
if status is not None:
|
|
43
|
+
for packed in status.details:
|
|
44
|
+
candidate = pb.ErrorResponse()
|
|
45
|
+
if packed.Is(candidate.DESCRIPTOR):
|
|
46
|
+
packed.Unpack(candidate)
|
|
47
|
+
details = candidate
|
|
48
|
+
break
|
|
49
|
+
except Exception as malformed:
|
|
50
|
+
translated = DexServiceError(
|
|
51
|
+
error.code(),
|
|
52
|
+
ErrorSubStatus.UNCATEGORIZED,
|
|
53
|
+
f"Dex returned malformed error details: {malformed}",
|
|
54
|
+
operation,
|
|
55
|
+
flow_id,
|
|
56
|
+
)
|
|
57
|
+
return translated
|
|
58
|
+
detail = (
|
|
59
|
+
details.detail if details is not None and details.detail else error.details()
|
|
60
|
+
) or str(error)
|
|
61
|
+
code = error.code()
|
|
62
|
+
sub_status = (
|
|
63
|
+
_map_sub_status(details.sub_status)
|
|
64
|
+
if details is not None
|
|
65
|
+
else ErrorSubStatus.UNCATEGORIZED
|
|
66
|
+
)
|
|
67
|
+
parameters = (code, sub_status, detail, operation, flow_id)
|
|
68
|
+
if sub_status is ErrorSubStatus.FLOW_ALREADY_STARTED:
|
|
69
|
+
return FlowAlreadyStartedError(*parameters)
|
|
70
|
+
if sub_status is ErrorSubStatus.FLOW_NOT_EXISTS:
|
|
71
|
+
if requirement == "existing":
|
|
72
|
+
return FlowNotFoundError(*parameters)
|
|
73
|
+
if requirement == "active":
|
|
74
|
+
return FlowNotActiveError(*parameters)
|
|
75
|
+
return DexServiceError(*parameters)
|
|
76
|
+
if sub_status is ErrorSubStatus.WORKER_API_ERROR:
|
|
77
|
+
if code is grpc.StatusCode.ABORTED:
|
|
78
|
+
return RpcLockConflictError(*parameters)
|
|
79
|
+
return WorkerInvocationError(
|
|
80
|
+
*parameters,
|
|
81
|
+
_worker_code(details),
|
|
82
|
+
details.original_worker_error_type if details is not None else "",
|
|
83
|
+
details.original_worker_error_detail if details is not None else "",
|
|
84
|
+
)
|
|
85
|
+
if sub_status is ErrorSubStatus.LONG_POLL_TIMEOUT:
|
|
86
|
+
return LongPollTimeoutError(*parameters)
|
|
87
|
+
return DexServiceError(*parameters)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _worker_code(details: pb.ErrorResponse | None) -> grpc.StatusCode | None:
|
|
91
|
+
if details is None or details.original_worker_error_status == 0:
|
|
92
|
+
return None
|
|
93
|
+
return next(
|
|
94
|
+
(
|
|
95
|
+
code
|
|
96
|
+
for code in grpc.StatusCode
|
|
97
|
+
if code.value[0] == details.original_worker_error_status
|
|
98
|
+
),
|
|
99
|
+
grpc.StatusCode.UNKNOWN,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def abort_worker_error(
|
|
104
|
+
context: grpc.ServicerContext,
|
|
105
|
+
error: BaseException,
|
|
106
|
+
) -> None:
|
|
107
|
+
context.abort_with_status(rpc_status.to_status(_worker_error_status(error)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def async_abort_worker_error(
|
|
111
|
+
context: grpc.aio.ServicerContext[Any, Any],
|
|
112
|
+
error: BaseException,
|
|
113
|
+
) -> None:
|
|
114
|
+
# types-grpcio on 3.11 omits aio abort_with_status; runtime provides it.
|
|
115
|
+
await cast(Any, context).abort_with_status(
|
|
116
|
+
rpc_status.to_status(_worker_error_status(error))
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _worker_error_status(error: BaseException) -> status_pb2.Status:
|
|
121
|
+
message = str(error) or type(error).__name__
|
|
122
|
+
worker_error = pb.WorkerErrorResponse(
|
|
123
|
+
detail=message,
|
|
124
|
+
error_type=f"{type(error).__module__}.{type(error).__qualname__}",
|
|
125
|
+
)
|
|
126
|
+
packed = any_pb2.Any()
|
|
127
|
+
packed.Pack(worker_error)
|
|
128
|
+
return status_pb2.Status(
|
|
129
|
+
code=grpc.StatusCode.UNKNOWN.value[0],
|
|
130
|
+
message=message,
|
|
131
|
+
details=[packed],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _map_sub_status(value: int) -> ErrorSubStatus:
|
|
136
|
+
statuses: dict[int, ErrorSubStatus] = {
|
|
137
|
+
int(pb.ERROR_SUB_STATUS_UNCATEGORIZED): ErrorSubStatus.UNCATEGORIZED,
|
|
138
|
+
int(
|
|
139
|
+
pb.ERROR_SUB_STATUS_FLOW_ALREADY_STARTED
|
|
140
|
+
): ErrorSubStatus.FLOW_ALREADY_STARTED,
|
|
141
|
+
int(pb.ERROR_SUB_STATUS_FLOW_NOT_EXISTS): ErrorSubStatus.FLOW_NOT_EXISTS,
|
|
142
|
+
int(pb.ERROR_SUB_STATUS_WORKER_API_ERROR): ErrorSubStatus.WORKER_API_ERROR,
|
|
143
|
+
int(pb.ERROR_SUB_STATUS_LONG_POLL_TIME_OUT): ErrorSubStatus.LONG_POLL_TIMEOUT,
|
|
144
|
+
}
|
|
145
|
+
return statuses.get(value, ErrorSubStatus.UNCATEGORIZED)
|
|
@@ -10,10 +10,11 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from typing import Any, Sequence, TypeVar, cast
|
|
13
|
+
from urllib.parse import unquote
|
|
13
14
|
|
|
14
15
|
from dex._utils import require_name
|
|
15
16
|
from dex._value_mapper import ValueMapper
|
|
16
|
-
from dex.attribute import Attribute, AttributeMap
|
|
17
|
+
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
17
18
|
from dex.channel import Channel, ChannelMap
|
|
18
19
|
from dex.codec import Codec
|
|
19
20
|
from dex.dexpb import dex_pb2 as pb
|
|
@@ -161,6 +162,7 @@ class InvocationContext:
|
|
|
161
162
|
)
|
|
162
163
|
if index is not None:
|
|
163
164
|
write.index_config.CopyFrom(index)
|
|
165
|
+
_apply_attribute_store_sync(write, definition)
|
|
164
166
|
self.attribute_writes[key] = write
|
|
165
167
|
|
|
166
168
|
def _delete_attribute(
|
|
@@ -177,6 +179,7 @@ class InvocationContext:
|
|
|
177
179
|
)
|
|
178
180
|
if index is not None:
|
|
179
181
|
write.index_config.CopyFrom(index)
|
|
182
|
+
_apply_attribute_store_sync(write, definition)
|
|
180
183
|
self.attribute_writes[key] = write
|
|
181
184
|
|
|
182
185
|
def _publish_channel(
|
|
@@ -202,6 +205,38 @@ class InvocationContext:
|
|
|
202
205
|
size=(current.size if current is not None else 0) + 1
|
|
203
206
|
)
|
|
204
207
|
|
|
208
|
+
def _attribute_map_keys(
|
|
209
|
+
self,
|
|
210
|
+
definition: AttributeMap[object],
|
|
211
|
+
) -> tuple[str, ...]:
|
|
212
|
+
self._require_registered(definition)
|
|
213
|
+
prefix = f"{definition.name}/"
|
|
214
|
+
physical_keys = {key for key in self._attributes if key.startswith(prefix)}
|
|
215
|
+
for key, write in self.attribute_writes.items():
|
|
216
|
+
if not key.startswith(prefix):
|
|
217
|
+
continue
|
|
218
|
+
if write.value.WhichOneof("kind") == "null_value":
|
|
219
|
+
physical_keys.discard(key)
|
|
220
|
+
else:
|
|
221
|
+
physical_keys.add(key)
|
|
222
|
+
return tuple(sorted(unquote(key[len(prefix) :]) for key in physical_keys))
|
|
223
|
+
|
|
224
|
+
def _channel_map_keys(
|
|
225
|
+
self,
|
|
226
|
+
definition: ChannelMap[object],
|
|
227
|
+
) -> tuple[str, ...]:
|
|
228
|
+
self._require_registered(definition)
|
|
229
|
+
if self._method is not InvocationMethod.RPC:
|
|
230
|
+
raise ValueError("ChannelMap introspection requires an RPC invocation")
|
|
231
|
+
prefix = f"{definition.name}/"
|
|
232
|
+
return tuple(
|
|
233
|
+
sorted(
|
|
234
|
+
unquote(key[len(prefix) :])
|
|
235
|
+
for key, info in self._channel_infos.items()
|
|
236
|
+
if key.startswith(prefix) and info.size > 0
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
205
240
|
def _channel_size(
|
|
206
241
|
self,
|
|
207
242
|
definition: Channel[object] | ChannelMap[object],
|