dex-python-sdk 0.1.1__tar.gz → 0.1.2__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.2}/PKG-INFO +57 -3
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/README.md +56 -2
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/__init__.py +18 -2
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_async_worker_dispatcher.py +59 -43
- dex_python_sdk-0.1.2/dex/_grpc_errors.py +145 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_invocation_context.py +3 -1
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_value_mapper.py +33 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_worker_dispatcher.py +61 -45
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/async_client.py +357 -30
- dex_python_sdk-0.1.2/dex/async_worker.py +189 -0
- dex_python_sdk-0.1.2/dex/attribute.py +243 -0
- dex_python_sdk-0.1.2/dex/blob_cache.py +145 -0
- dex_python_sdk-0.1.2/dex/channel.py +342 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/client.py +350 -24
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/client_options.py +9 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/codec.py +143 -4
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/condition.py +17 -0
- dex_python_sdk-0.1.2/dex/context.py +169 -0
- dex_python_sdk-0.1.2/dex/dexpb/dex_pb2.py +395 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/dexpb/dex_pb2.pyi +82 -31
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/dexpb/dex_pb2_grpc.py +43 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/flow.py +216 -23
- dex_python_sdk-0.1.2/dex/flow_config.py +59 -0
- dex_python_sdk-0.1.2/dex/flow_info.py +107 -0
- dex_python_sdk-0.1.2/dex/flow_options.py +214 -0
- dex_python_sdk-0.1.2/dex/runtime_errors.py +285 -0
- dex_python_sdk-0.1.2/dex/step.py +427 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/step_execution.py +36 -0
- dex_python_sdk-0.1.2/dex/timer.py +44 -0
- dex_python_sdk-0.1.2/dex/wait.py +108 -0
- dex_python_sdk-0.1.2/dex/worker.py +214 -0
- dex_python_sdk-0.1.2/dex/worker_options.py +52 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/pyproject.toml +1 -1
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/Cargo.lock +140 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/Cargo.toml +5 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/config.rs +31 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/error.rs +11 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/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.2}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_utils.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/dex/py.typed +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.1.1 → dex_python_sdk-0.1.2}/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.2
|
|
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,31 @@ 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
|
+
### Errors
|
|
139
|
+
|
|
140
|
+
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
141
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
|
|
142
|
+
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
143
|
+
waits, config updates, and continue-as-new triggers raise
|
|
144
|
+
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
try:
|
|
148
|
+
client.publish(flow_id, orders.approved, order_id)
|
|
149
|
+
except dex.FlowNotActiveError:
|
|
150
|
+
# The Flow is missing or already closed.
|
|
151
|
+
pass
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
|
|
155
|
+
raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
|
|
156
|
+
`RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
|
|
157
|
+
`code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
|
|
158
|
+
exception through Python exception chaining. Worker failures also expose
|
|
159
|
+
`worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
|
|
160
|
+
serialization, and invalid handler returns use `FlowDefinitionError`,
|
|
161
|
+
`ValueMappingError`, and `InvalidStepResultError`.
|
|
162
|
+
|
|
120
163
|
### Sync vs asyncio
|
|
121
164
|
|
|
122
165
|
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
@@ -209,7 +252,18 @@ make -C ../protos proto-python
|
|
|
209
252
|
Checked-in Python stubs land in `dex/dexpb/`.
|
|
210
253
|
#### Linting
|
|
211
254
|
|
|
212
|
-
|
|
255
|
+
Validate that every `dex.__all__` class, function, constant, public method,
|
|
256
|
+
argument, return value, dataclass field, enum value, and public instance
|
|
257
|
+
attribute has a Google-style docstring:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
uv run --frozen python scripts/check_public_docs.py
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The checker resolves definitions from the public package export table, so
|
|
264
|
+
private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
|
|
265
|
+
or IDE hover information to read the same documentation. To run all other
|
|
266
|
+
linting for this project:
|
|
213
267
|
|
|
214
268
|
```bash
|
|
215
269
|
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,31 @@ 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
|
+
### Errors
|
|
123
|
+
|
|
124
|
+
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
125
|
+
(`get_attribute`, `describe_flow`, `wait_for_flow`, and `reset_flow`) raise
|
|
126
|
+
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
127
|
+
waits, config updates, and continue-as-new triggers raise
|
|
128
|
+
`FlowNotActiveError` when no running Flow can accept the operation.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
try:
|
|
132
|
+
client.publish(flow_id, orders.approved, order_id)
|
|
133
|
+
except dex.FlowNotActiveError:
|
|
134
|
+
# The Flow is missing or already closed.
|
|
135
|
+
pass
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Duplicate starts, worker failures, RPC lock contention, and long-poll timeouts
|
|
139
|
+
raise `FlowAlreadyStartedError`, `WorkerInvocationError`,
|
|
140
|
+
`RpcLockConflictError`, and `LongPollTimeoutError`. All service errors retain
|
|
141
|
+
`code`, `sub_status`, `detail`, `operation`, `flow_id`, and the original gRPC
|
|
142
|
+
exception through Python exception chaining. Worker failures also expose
|
|
143
|
+
`worker_code`, `worker_error_type`, and `worker_error_detail`. Registration,
|
|
144
|
+
serialization, and invalid handler returns use `FlowDefinitionError`,
|
|
145
|
+
`ValueMappingError`, and `InvalidStepResultError`.
|
|
146
|
+
|
|
104
147
|
### Sync vs asyncio
|
|
105
148
|
|
|
106
149
|
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
@@ -193,7 +236,18 @@ make -C ../protos proto-python
|
|
|
193
236
|
Checked-in Python stubs land in `dex/dexpb/`.
|
|
194
237
|
#### Linting
|
|
195
238
|
|
|
196
|
-
|
|
239
|
+
Validate that every `dex.__all__` class, function, constant, public method,
|
|
240
|
+
argument, return value, dataclass field, enum value, and public instance
|
|
241
|
+
attribute has a Google-style docstring:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
uv run --frozen python scripts/check_public_docs.py
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The checker resolves definitions from the public package export table, so
|
|
248
|
+
private helpers and generated protobuf modules are excluded. Use `help(dex.Client)`
|
|
249
|
+
or IDE hover information to read the same documentation. To run all other
|
|
250
|
+
linting for this project:
|
|
197
251
|
|
|
198
252
|
```bash
|
|
199
253
|
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,
|
|
@@ -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,10 +153,12 @@ __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",
|
|
@@ -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)
|
|
@@ -13,7 +13,7 @@ from typing import Any, Sequence, TypeVar, cast
|
|
|
13
13
|
|
|
14
14
|
from dex._utils import require_name
|
|
15
15
|
from dex._value_mapper import ValueMapper
|
|
16
|
-
from dex.attribute import Attribute, AttributeMap
|
|
16
|
+
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
17
17
|
from dex.channel import Channel, ChannelMap
|
|
18
18
|
from dex.codec import Codec
|
|
19
19
|
from dex.dexpb import dex_pb2 as pb
|
|
@@ -161,6 +161,7 @@ class InvocationContext:
|
|
|
161
161
|
)
|
|
162
162
|
if index is not None:
|
|
163
163
|
write.index_config.CopyFrom(index)
|
|
164
|
+
_apply_attribute_store_sync(write, definition)
|
|
164
165
|
self.attribute_writes[key] = write
|
|
165
166
|
|
|
166
167
|
def _delete_attribute(
|
|
@@ -177,6 +178,7 @@ class InvocationContext:
|
|
|
177
178
|
)
|
|
178
179
|
if index is not None:
|
|
179
180
|
write.index_config.CopyFrom(index)
|
|
181
|
+
_apply_attribute_store_sync(write, definition)
|
|
180
182
|
self.attribute_writes[key] = write
|
|
181
183
|
|
|
182
184
|
def _publish_channel(
|
|
@@ -19,6 +19,7 @@ from google.protobuf import struct_pb2
|
|
|
19
19
|
from dex.attribute import AttributeIndex, IndexType
|
|
20
20
|
from dex.codec import Codec, CodecRegistry, Value, WireKind
|
|
21
21
|
from dex.dexpb import dex_pb2 as pb
|
|
22
|
+
from dex.runtime_errors import ValueMappingError
|
|
22
23
|
|
|
23
24
|
_JSON_ENCODING = "json"
|
|
24
25
|
_RAW_BYTES_ENCODING = "rawbytes"
|
|
@@ -32,6 +33,14 @@ class ValueMapper:
|
|
|
32
33
|
return self._codecs.resolve(value_type)
|
|
33
34
|
|
|
34
35
|
def encode(self, value: Any, codec: Codec[Any]) -> pb.Value:
|
|
36
|
+
try:
|
|
37
|
+
return self._encode(value, codec)
|
|
38
|
+
except ValueMappingError:
|
|
39
|
+
raise
|
|
40
|
+
except Exception as error:
|
|
41
|
+
raise ValueMappingError("encode", str(error)) from error
|
|
42
|
+
|
|
43
|
+
def _encode(self, value: Any, codec: Codec[Any]) -> pb.Value:
|
|
35
44
|
if value is None:
|
|
36
45
|
return pb.Value(
|
|
37
46
|
obj_value=pb.EncodedObject(
|
|
@@ -66,6 +75,14 @@ class ValueMapper:
|
|
|
66
75
|
raise TypeError(f"unsupported wire kind {logical.kind}")
|
|
67
76
|
|
|
68
77
|
def encode_dynamic(self, value: Any) -> pb.Value:
|
|
78
|
+
try:
|
|
79
|
+
return self._encode_dynamic(value)
|
|
80
|
+
except ValueMappingError:
|
|
81
|
+
raise
|
|
82
|
+
except Exception as error:
|
|
83
|
+
raise ValueMappingError("encode", str(error)) from error
|
|
84
|
+
|
|
85
|
+
def _encode_dynamic(self, value: Any) -> pb.Value:
|
|
69
86
|
if value is None:
|
|
70
87
|
return self.encode(value, self._codecs.resolve(type(None)))
|
|
71
88
|
value_type = type(value)
|
|
@@ -83,6 +100,14 @@ class ValueMapper:
|
|
|
83
100
|
return self.encode(value, codec)
|
|
84
101
|
|
|
85
102
|
def decode(self, value: pb.Value, codec: Codec[Any]) -> Any:
|
|
103
|
+
try:
|
|
104
|
+
return self._decode(value, codec)
|
|
105
|
+
except ValueMappingError:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as error:
|
|
108
|
+
raise ValueMappingError("decode", str(error)) from error
|
|
109
|
+
|
|
110
|
+
def _decode(self, value: pb.Value, codec: Codec[Any]) -> Any:
|
|
86
111
|
kind = value.WhichOneof("kind")
|
|
87
112
|
expected = codec.wire_kind
|
|
88
113
|
if kind == "string_value" and expected is WireKind.STRING:
|
|
@@ -113,6 +138,14 @@ class ValueMapper:
|
|
|
113
138
|
raise TypeError(f"cannot decode {kind or 'empty Value'} as {codec.type_name}")
|
|
114
139
|
|
|
115
140
|
def to_value(self, value: pb.Value) -> Value:
|
|
141
|
+
try:
|
|
142
|
+
return self._to_value(value)
|
|
143
|
+
except ValueMappingError:
|
|
144
|
+
raise
|
|
145
|
+
except Exception as error:
|
|
146
|
+
raise ValueMappingError("decode", str(error)) from error
|
|
147
|
+
|
|
148
|
+
def _to_value(self, value: pb.Value) -> Value:
|
|
116
149
|
kind = value.WhichOneof("kind")
|
|
117
150
|
if kind == "string_value":
|
|
118
151
|
return Value(WireKind.STRING, value.string_value)
|