dex-python-sdk 0.0.2__tar.gz → 0.1.1__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.0.2 → dex_python_sdk-0.1.1}/PKG-INFO +58 -19
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/README.md +57 -17
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/__init__.py +12 -1
- dex_python_sdk-0.1.1/dex/_async_value_hydrator.py +223 -0
- dex_python_sdk-0.1.1/dex/_async_worker_dispatcher.py +139 -0
- dex_python_sdk-0.1.1/dex/_async_worker_service.py +65 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_grpc_errors.py +16 -3
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_value_mapper.py +23 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_worker_dispatcher.py +13 -0
- dex_python_sdk-0.1.1/dex/async_client.py +757 -0
- dex_python_sdk-0.1.1/dex/async_worker.py +106 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/client.py +43 -1
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/flow.py +40 -8
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/flow_info.py +8 -2
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/pyproject.toml +12 -3
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/Cargo.lock +159 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/Cargo.toml +3 -0
- dex_python_sdk-0.0.2/dex/command_request.py +0 -120
- dex_python_sdk-0.0.2/dex/command_results.py +0 -107
- dex_python_sdk-0.0.2/dex/communication.py +0 -136
- dex_python_sdk-0.0.2/dex/communication_schema.py +0 -54
- dex_python_sdk-0.0.2/dex/data_attributes.py +0 -70
- dex_python_sdk-0.0.2/dex/errors.py +0 -109
- dex_python_sdk-0.0.2/dex/object_encoder.py +0 -799
- dex_python_sdk-0.0.2/dex/persistence.py +0 -89
- dex_python_sdk-0.0.2/dex/persistence_options.py +0 -12
- dex_python_sdk-0.0.2/dex/persistence_schema.py +0 -51
- dex_python_sdk-0.0.2/dex/registry.py +0 -204
- dex_python_sdk-0.0.2/dex/reset_workflow_type_and_options.py +0 -67
- dex_python_sdk-0.0.2/dex/rpc.py +0 -93
- dex_python_sdk-0.0.2/dex/search_attributes.py +0 -184
- dex_python_sdk-0.0.2/dex/state_decision.py +0 -153
- dex_python_sdk-0.0.2/dex/state_execution_locals.py +0 -66
- dex_python_sdk-0.0.2/dex/state_movement.py +0 -115
- dex_python_sdk-0.0.2/dex/state_schema.py +0 -48
- dex_python_sdk-0.0.2/dex/stop_workflow_options.py +0 -18
- dex_python_sdk-0.0.2/dex/tests/__init__.py +0 -80
- dex_python_sdk-0.0.2/dex/tests/dex-service-env/docker-compose-init.sh +0 -44
- dex_python_sdk-0.0.2/dex/tests/dex-service-env/docker-compose.yml +0 -97
- dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/README.md +0 -39
- dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +0 -9
- dex_python_sdk-0.0.2/dex/tests/dex-service-env/dynamicconfig/docker.yaml +0 -2
- dex_python_sdk-0.0.2/dex/tests/test_abnormal_exit_workflow.py +0 -43
- dex_python_sdk-0.0.2/dex/tests/test_basic_workflow.py +0 -70
- dex_python_sdk-0.0.2/dex/tests/test_conditional_complete.py +0 -50
- dex_python_sdk-0.0.2/dex/tests/test_describe_workflow.py +0 -40
- dex_python_sdk-0.0.2/dex/tests/test_empty_data_decodes_properly.py +0 -74
- dex_python_sdk-0.0.2/dex/tests/test_internal_channel.py +0 -28
- dex_python_sdk-0.0.2/dex/tests/test_internal_channel_with_no_prefix_channel.py +0 -41
- dex_python_sdk-0.0.2/dex/tests/test_persistence_data_attributes.py +0 -62
- dex_python_sdk-0.0.2/dex/tests/test_persistence_search_attributes.py +0 -127
- dex_python_sdk-0.0.2/dex/tests/test_persistence_state_execution_locals.py +0 -38
- dex_python_sdk-0.0.2/dex/tests/test_rpc.py +0 -64
- dex_python_sdk-0.0.2/dex/tests/test_rpc_with_memo.py +0 -195
- dex_python_sdk-0.0.2/dex/tests/test_rpc_with_memo_duplicate_java_tests.py +0 -117
- dex_python_sdk-0.0.2/dex/tests/test_signal.py +0 -51
- dex_python_sdk-0.0.2/dex/tests/test_skip_wait_until.py +0 -76
- dex_python_sdk-0.0.2/dex/tests/test_state_failure_recovery.py +0 -28
- dex_python_sdk-0.0.2/dex/tests/test_timer.py +0 -35
- dex_python_sdk-0.0.2/dex/tests/test_wait_for_state_execution_completion.py +0 -53
- dex_python_sdk-0.0.2/dex/tests/test_workflow_errors.py +0 -87
- dex_python_sdk-0.0.2/dex/tests/test_workflow_state_options.py +0 -118
- dex_python_sdk-0.0.2/dex/tests/test_workflow_state_options_override.py +0 -44
- dex_python_sdk-0.0.2/dex/tests/worker_server.py +0 -64
- dex_python_sdk-0.0.2/dex/tests/workflows/abnormal_exit_workflow.py +0 -42
- dex_python_sdk-0.0.2/dex/tests/workflows/basic_workflow.py +0 -62
- dex_python_sdk-0.0.2/dex/tests/workflows/conditional_complete_workflow.py +0 -95
- dex_python_sdk-0.0.2/dex/tests/workflows/describe_workflow.py +0 -46
- dex_python_sdk-0.0.2/dex/tests/workflows/empty_data_workflow.py +0 -45
- dex_python_sdk-0.0.2/dex/tests/workflows/internal_channel_workflow.py +0 -129
- dex_python_sdk-0.0.2/dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +0 -100
- dex_python_sdk-0.0.2/dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +0 -276
- dex_python_sdk-0.0.2/dex/tests/workflows/persistence_data_attributes_workflow.py +0 -98
- dex_python_sdk-0.0.2/dex/tests/workflows/persistence_search_attributes_workflow.py +0 -159
- dex_python_sdk-0.0.2/dex/tests/workflows/persistence_state_execution_local_workflow.py +0 -63
- dex_python_sdk-0.0.2/dex/tests/workflows/recovery_workflow.py +0 -82
- dex_python_sdk-0.0.2/dex/tests/workflows/rpc_memo_workflow.py +0 -231
- dex_python_sdk-0.0.2/dex/tests/workflows/rpc_workflow.py +0 -117
- dex_python_sdk-0.0.2/dex/tests/workflows/state_options_override_workflow.py +0 -93
- dex_python_sdk-0.0.2/dex/tests/workflows/state_options_workflow.py +0 -84
- dex_python_sdk-0.0.2/dex/tests/workflows/timer_workflow.py +0 -46
- dex_python_sdk-0.0.2/dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +0 -70
- dex_python_sdk-0.0.2/dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +0 -71
- dex_python_sdk-0.0.2/dex/tests/workflows/wait_internal_channel_workflow.py +0 -47
- dex_python_sdk-0.0.2/dex/tests/workflows/wait_signal_workflow.py +0 -147
- dex_python_sdk-0.0.2/dex/type_store.py +0 -99
- dex_python_sdk-0.0.2/dex/unregistered_client.py +0 -585
- dex_python_sdk-0.0.2/dex/utils/__init__.py +0 -3
- dex_python_sdk-0.0.2/dex/utils/dex_typing.py +0 -25
- dex_python_sdk-0.0.2/dex/utils/persistence_utils.py +0 -32
- dex_python_sdk-0.0.2/dex/worker_service.py +0 -432
- dex_python_sdk-0.0.2/dex/workflow.py +0 -79
- dex_python_sdk-0.0.2/dex/workflow_context.py +0 -44
- dex_python_sdk-0.0.2/dex/workflow_info.py +0 -16
- dex_python_sdk-0.0.2/dex/workflow_options.py +0 -74
- dex_python_sdk-0.0.2/dex/workflow_state.py +0 -123
- dex_python_sdk-0.0.2/dex/workflow_state_options.py +0 -154
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/LICENSE +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_utils.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/attribute.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/channel.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/client_options.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/codec.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/condition.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/context.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/dexpb/dex_pb2.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/dexpb/dex_pb2.pyi +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/flow_options.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/py.typed +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/step.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/timer.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/wait.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/worker.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.0.2 → dex_python_sdk-0.1.1}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
|
-
Requires-Dist: httpx==0.28.1
|
|
7
6
|
Requires-Dist: protobuf>=7.35.1
|
|
8
7
|
License-File: LICENSE
|
|
9
8
|
License-File: LEGACY_NOTICES.md
|
|
@@ -88,7 +87,7 @@ options = (
|
|
|
88
87
|
```
|
|
89
88
|
|
|
90
89
|
```
|
|
91
|
-
pip install dex-python-sdk==0.0
|
|
90
|
+
pip install dex-python-sdk==0.1.0
|
|
92
91
|
```
|
|
93
92
|
|
|
94
93
|
See [samples](../examples/python) for use case examples.
|
|
@@ -100,14 +99,16 @@ See [samples](../examples/python) for use case examples.
|
|
|
100
99
|
|
|
101
100
|
## Concepts
|
|
102
101
|
|
|
103
|
-
Applications implement two generic interfaces from [`dex
|
|
102
|
+
Applications implement two generic interfaces from [`dex`](dex/):
|
|
104
103
|
|
|
105
104
|
- `Flow[START_INPUT]` returns `StepList.start_step(...)`, followed by optional
|
|
106
105
|
`.other_steps(...)`, from one `get_steps()` method. The `StepList` generic
|
|
107
106
|
binds the Flow input to the starting Step input. Use `StepList.empty()` when
|
|
108
107
|
a Flow has no Steps.
|
|
109
|
-
- `Step[INPUT]` implements
|
|
110
|
-
`
|
|
108
|
+
- `Step[INPUT]` implements `execute` and optionally `wait_for`. The default
|
|
109
|
+
Worker path requires synchronous handlers. With `AsyncWorker` and
|
|
110
|
+
`Registry(..., allow_async_handlers=True)`, handlers may be `async def` and
|
|
111
|
+
`await` an `AsyncClient`.
|
|
111
112
|
|
|
112
113
|
`StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
|
|
113
114
|
two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
@@ -116,16 +117,30 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
|
116
117
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
117
118
|
instead of raw Flow, Step, or RPC strings.
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
### Sync vs asyncio
|
|
121
|
+
|
|
122
|
+
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
123
|
+
Worker. Blocking `Client` calls inside `Step.execute` are safe (one pool
|
|
124
|
+
thread is occupied; other RPCs still run).
|
|
125
|
+
- **Asyncio:** `AsyncClient` and `AsyncWorker` use `grpc.aio`. Use
|
|
126
|
+
`Registry(..., allow_async_handlers=True)` when Steps/RPCs are coroutines.
|
|
127
|
+
Inside async `execute`, inject `AsyncClient` — do not call sync `Client` on
|
|
128
|
+
the Worker event loop. Sync `Worker` still rejects coroutine handlers at
|
|
129
|
+
registry construction unless `allow_async_handlers=True` (and even then the
|
|
130
|
+
sync Worker dispatcher rejects awaitable return values).
|
|
131
|
+
|
|
132
|
+
Integration scenarios live under
|
|
133
|
+
[`tests/integ`](tests/integ/README.md). They exercise the same workflows,
|
|
134
|
+
client operations, and assertions as the Java suite against an isolated
|
|
135
|
+
`dexcli dev` environment.
|
|
123
136
|
|
|
124
137
|
## Implementation status
|
|
125
138
|
|
|
126
|
-
The strongly typed contracts, registry, synchronous Client,
|
|
127
|
-
|
|
128
|
-
transport; the native bridge is limited to
|
|
139
|
+
The strongly typed contracts, registry, synchronous Client/Worker, optional
|
|
140
|
+
`AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
|
|
141
|
+
implemented. Python owns its gRPC transport; the native bridge is limited to
|
|
142
|
+
the shared BlobCache. Design notes:
|
|
143
|
+
[`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
|
|
129
144
|
|
|
130
145
|
## Running dex-server locally
|
|
131
146
|
|
|
@@ -162,6 +177,25 @@ environment:
|
|
|
162
177
|
./run-integration-tests.sh
|
|
163
178
|
```
|
|
164
179
|
|
|
180
|
+
### Measure integration coverage
|
|
181
|
+
|
|
182
|
+
Run the same integration suite with Python source coverage:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
./run-integration-tests.sh --coverage
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Only the integration scenarios contribute execution data, and only production
|
|
189
|
+
Python modules under `dex` are measured. Generated protobuf modules under
|
|
190
|
+
`dex/dexpb` are excluded. The
|
|
191
|
+
terminal report lists uncovered line ranges. The browser report starts at
|
|
192
|
+
`coverage/html/index.html`; `coverage/coverage.xml` and `coverage/lcov.info`
|
|
193
|
+
are also generated.
|
|
194
|
+
|
|
195
|
+
CI uploads LCOV to Codecov with GitHub OIDC under the
|
|
196
|
+
`sdk-python-integration` flag and retains the full report as the
|
|
197
|
+
`sdk-python-integration-coverage` Actions artifact.
|
|
198
|
+
|
|
165
199
|
#### Update IDL
|
|
166
200
|
|
|
167
201
|
Edit [`protos/dex.proto`](../protos/dex.proto). Rename catalog: [`docs/design/idl-renames.md`](../docs/design/idl-renames.md).
|
|
@@ -186,12 +220,17 @@ This project is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.m
|
|
|
186
220
|
|
|
187
221
|
## Publishing to PyPI
|
|
188
222
|
|
|
189
|
-
1.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
1. Optionally run **Publish Python SDK to PyPI** via workflow_dispatch with a version and
|
|
224
|
+
`publish=false` to validate all distributions without uploading.
|
|
225
|
+
2. Create a GitHub Release with tag `sdk-python/vX.Y.Z` (for example `sdk-python/v0.1.0`).
|
|
226
|
+
CI stamps that version into `pyproject.toml` for the build (same idea as the TypeScript
|
|
227
|
+
SDK release), then builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and
|
|
228
|
+
Windows x86_64 wheels, verifies the source distribution, and publishes with `PYPI_TOKEN`.
|
|
229
|
+
3. After publishing, bump the committed `pyproject.toml` / docs install line when you want
|
|
230
|
+
the repo tip to reflect the released version.
|
|
231
|
+
|
|
232
|
+
A manual run publishes only from `main`, and only when `publish` is explicitly selected.
|
|
233
|
+
The dispatch `version` input is stamped the same way as a release tag.
|
|
195
234
|
|
|
196
235
|
See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo tag conventions.
|
|
197
236
|
|
|
@@ -71,7 +71,7 @@ options = (
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
pip install dex-python-sdk==0.0
|
|
74
|
+
pip install dex-python-sdk==0.1.0
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
See [samples](../examples/python) for use case examples.
|
|
@@ -83,14 +83,16 @@ See [samples](../examples/python) for use case examples.
|
|
|
83
83
|
|
|
84
84
|
## Concepts
|
|
85
85
|
|
|
86
|
-
Applications implement two generic interfaces from [`dex
|
|
86
|
+
Applications implement two generic interfaces from [`dex`](dex/):
|
|
87
87
|
|
|
88
88
|
- `Flow[START_INPUT]` returns `StepList.start_step(...)`, followed by optional
|
|
89
89
|
`.other_steps(...)`, from one `get_steps()` method. The `StepList` generic
|
|
90
90
|
binds the Flow input to the starting Step input. Use `StepList.empty()` when
|
|
91
91
|
a Flow has no Steps.
|
|
92
|
-
- `Step[INPUT]` implements
|
|
93
|
-
`
|
|
92
|
+
- `Step[INPUT]` implements `execute` and optionally `wait_for`. The default
|
|
93
|
+
Worker path requires synchronous handlers. With `AsyncWorker` and
|
|
94
|
+
`Registry(..., allow_async_handlers=True)`, handlers may be `async def` and
|
|
95
|
+
`await` an `AsyncClient`.
|
|
94
96
|
|
|
95
97
|
`StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
|
|
96
98
|
two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
@@ -99,16 +101,30 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
|
99
101
|
codec before Client or Worker startup. `Client` methods use these typed objects
|
|
100
102
|
instead of raw Flow, Step, or RPC strings.
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
### Sync vs asyncio
|
|
105
|
+
|
|
106
|
+
- **Sync (default):** `Client` and `Worker` use blocking gRPC and a thread-pool
|
|
107
|
+
Worker. Blocking `Client` calls inside `Step.execute` are safe (one pool
|
|
108
|
+
thread is occupied; other RPCs still run).
|
|
109
|
+
- **Asyncio:** `AsyncClient` and `AsyncWorker` use `grpc.aio`. Use
|
|
110
|
+
`Registry(..., allow_async_handlers=True)` when Steps/RPCs are coroutines.
|
|
111
|
+
Inside async `execute`, inject `AsyncClient` — do not call sync `Client` on
|
|
112
|
+
the Worker event loop. Sync `Worker` still rejects coroutine handlers at
|
|
113
|
+
registry construction unless `allow_async_handlers=True` (and even then the
|
|
114
|
+
sync Worker dispatcher rejects awaitable return values).
|
|
115
|
+
|
|
116
|
+
Integration scenarios live under
|
|
117
|
+
[`tests/integ`](tests/integ/README.md). They exercise the same workflows,
|
|
118
|
+
client operations, and assertions as the Java suite against an isolated
|
|
119
|
+
`dexcli dev` environment.
|
|
106
120
|
|
|
107
121
|
## Implementation status
|
|
108
122
|
|
|
109
|
-
The strongly typed contracts, registry, synchronous Client,
|
|
110
|
-
|
|
111
|
-
transport; the native bridge is limited to
|
|
123
|
+
The strongly typed contracts, registry, synchronous Client/Worker, optional
|
|
124
|
+
`AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
|
|
125
|
+
implemented. Python owns its gRPC transport; the native bridge is limited to
|
|
126
|
+
the shared BlobCache. Design notes:
|
|
127
|
+
[`docs/design/plan/python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md).
|
|
112
128
|
|
|
113
129
|
## Running dex-server locally
|
|
114
130
|
|
|
@@ -145,6 +161,25 @@ environment:
|
|
|
145
161
|
./run-integration-tests.sh
|
|
146
162
|
```
|
|
147
163
|
|
|
164
|
+
### Measure integration coverage
|
|
165
|
+
|
|
166
|
+
Run the same integration suite with Python source coverage:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
./run-integration-tests.sh --coverage
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Only the integration scenarios contribute execution data, and only production
|
|
173
|
+
Python modules under `dex` are measured. Generated protobuf modules under
|
|
174
|
+
`dex/dexpb` are excluded. The
|
|
175
|
+
terminal report lists uncovered line ranges. The browser report starts at
|
|
176
|
+
`coverage/html/index.html`; `coverage/coverage.xml` and `coverage/lcov.info`
|
|
177
|
+
are also generated.
|
|
178
|
+
|
|
179
|
+
CI uploads LCOV to Codecov with GitHub OIDC under the
|
|
180
|
+
`sdk-python-integration` flag and retains the full report as the
|
|
181
|
+
`sdk-python-integration-coverage` Actions artifact.
|
|
182
|
+
|
|
148
183
|
#### Update IDL
|
|
149
184
|
|
|
150
185
|
Edit [`protos/dex.proto`](../protos/dex.proto). Rename catalog: [`docs/design/idl-renames.md`](../docs/design/idl-renames.md).
|
|
@@ -169,12 +204,17 @@ This project is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.m
|
|
|
169
204
|
|
|
170
205
|
## Publishing to PyPI
|
|
171
206
|
|
|
172
|
-
1.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
207
|
+
1. Optionally run **Publish Python SDK to PyPI** via workflow_dispatch with a version and
|
|
208
|
+
`publish=false` to validate all distributions without uploading.
|
|
209
|
+
2. Create a GitHub Release with tag `sdk-python/vX.Y.Z` (for example `sdk-python/v0.1.0`).
|
|
210
|
+
CI stamps that version into `pyproject.toml` for the build (same idea as the TypeScript
|
|
211
|
+
SDK release), then builds and smoke-tests Linux x86_64/ARM64, macOS x86_64/ARM64, and
|
|
212
|
+
Windows x86_64 wheels, verifies the source distribution, and publishes with `PYPI_TOKEN`.
|
|
213
|
+
3. After publishing, bump the committed `pyproject.toml` / docs install line when you want
|
|
214
|
+
the repo tip to reflect the released version.
|
|
215
|
+
|
|
216
|
+
A manual run publishes only from `main`, and only when `publish` is explicitly selected.
|
|
217
|
+
The dispatch `version` input is stamped the same way as a release tag.
|
|
178
218
|
|
|
179
219
|
See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo tag conventions.
|
|
180
220
|
|
|
@@ -17,6 +17,8 @@ from dex.attribute import (
|
|
|
17
17
|
)
|
|
18
18
|
from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
|
|
19
19
|
from dex.channel import Channel, ChannelMap
|
|
20
|
+
from dex.async_client import AsyncClient
|
|
21
|
+
from dex.async_worker import AsyncWorker
|
|
20
22
|
from dex.client import Client
|
|
21
23
|
from dex.client_options import ClientOptions
|
|
22
24
|
from dex.codec import (
|
|
@@ -35,7 +37,13 @@ from dex.condition import ConditionCombination
|
|
|
35
37
|
from dex.context import Context
|
|
36
38
|
from dex.flow import Flow, PersistenceSchema, Registry, RPCResult, rpc
|
|
37
39
|
from dex.flow_config import ActiveStepSearchMode, FlowConfig
|
|
38
|
-
from dex.flow_info import
|
|
40
|
+
from dex.flow_info import (
|
|
41
|
+
FlowInfo,
|
|
42
|
+
FlowStatus,
|
|
43
|
+
HealthInfo,
|
|
44
|
+
SearchFlowEntry,
|
|
45
|
+
SearchFlowsPage,
|
|
46
|
+
)
|
|
39
47
|
from dex.flow_options import (
|
|
40
48
|
IdReusePolicy,
|
|
41
49
|
ResetFlowOptions,
|
|
@@ -85,6 +93,8 @@ __all__ = [
|
|
|
85
93
|
"AttributeIndex",
|
|
86
94
|
"AttributeLock",
|
|
87
95
|
"AttributeMap",
|
|
96
|
+
"AsyncClient",
|
|
97
|
+
"AsyncWorker",
|
|
88
98
|
"BlobCache",
|
|
89
99
|
"BlobCacheConfig",
|
|
90
100
|
"Channel",
|
|
@@ -115,6 +125,7 @@ __all__ = [
|
|
|
115
125
|
"ResetType",
|
|
116
126
|
"RetryPolicy",
|
|
117
127
|
"SearchFlowEntry",
|
|
128
|
+
"SearchFlowsPage",
|
|
118
129
|
"StartFlowOptions",
|
|
119
130
|
"StepExecutionId",
|
|
120
131
|
"StepDecision",
|
|
@@ -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 AsyncValueHydrator:
|
|
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
|
+
async def hydrate(self, value: pb.Value) -> pb.Value:
|
|
40
|
+
return (await self.hydrate_all([value]))[0]
|
|
41
|
+
|
|
42
|
+
async 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
|
+
await 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
|
+
async 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 = await 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
|
+
async 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(await 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
|
+
async 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 = await 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
|
+
async 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 = await 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
|
+
async def _load_misses(self, misses: list[_PendingBlob]) -> None:
|
|
135
|
+
if not misses:
|
|
136
|
+
return
|
|
137
|
+
response = await 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
|
+
AsyncValueHydrator._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")
|
|
@@ -0,0 +1,139 @@
|
|
|
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 inspect import isawaitable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from dex._async_value_hydrator import AsyncValueHydrator
|
|
15
|
+
from dex._invocation_context import InvocationContext, InvocationMethod
|
|
16
|
+
from dex._value_mapper import ValueMapper
|
|
17
|
+
from dex._worker_dispatcher import WorkerDispatcher
|
|
18
|
+
from dex.dexpb import dex_pb2 as pb
|
|
19
|
+
from dex.flow import RPCResult, Registry
|
|
20
|
+
from dex.step import StepDecision
|
|
21
|
+
from dex.wait import Wait
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
registry: Registry,
|
|
28
|
+
values: ValueMapper,
|
|
29
|
+
hydrator: AsyncValueHydrator,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._registry = registry
|
|
32
|
+
self._values = values
|
|
33
|
+
self._hydrator = hydrator # type: ignore[assignment]
|
|
34
|
+
|
|
35
|
+
async def invoke_wait_for( # type: ignore[override]
|
|
36
|
+
self,
|
|
37
|
+
original: pb.InvokeWaitForMethodRequest,
|
|
38
|
+
) -> pb.InvokeWaitForMethodResponse:
|
|
39
|
+
request = await self._hydrator.wait_for_request(original)
|
|
40
|
+
flow = self._registry._flow_by_type(request.flow_type)
|
|
41
|
+
step = flow.step(request.step_type)
|
|
42
|
+
context = InvocationContext(
|
|
43
|
+
InvocationMethod.WAIT_FOR,
|
|
44
|
+
flow,
|
|
45
|
+
request.context,
|
|
46
|
+
self._values,
|
|
47
|
+
request.attributes,
|
|
48
|
+
)
|
|
49
|
+
input = self._values.decode(request.step_input, step.input_codec)
|
|
50
|
+
wait = step.step.wait_for(context, input)
|
|
51
|
+
if isawaitable(wait):
|
|
52
|
+
wait = await wait
|
|
53
|
+
if not isinstance(wait, Wait):
|
|
54
|
+
raise TypeError("wait_for must return Wait")
|
|
55
|
+
response = pb.InvokeWaitForMethodResponse(
|
|
56
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
57
|
+
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
58
|
+
record_events=context.events,
|
|
59
|
+
publish_to_channel=context.publications,
|
|
60
|
+
)
|
|
61
|
+
waiting = self._map_wait(flow, wait)
|
|
62
|
+
if waiting is not None:
|
|
63
|
+
response.waiting_condition.CopyFrom(waiting)
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
async def invoke_execute( # type: ignore[override]
|
|
67
|
+
self,
|
|
68
|
+
original: pb.InvokeExecuteMethodRequest,
|
|
69
|
+
) -> pb.InvokeExecuteMethodResponse:
|
|
70
|
+
request = await self._hydrator.execute_request(original)
|
|
71
|
+
flow = self._registry._flow_by_type(request.flow_type)
|
|
72
|
+
step = flow.step(request.step_type)
|
|
73
|
+
condition_results = (
|
|
74
|
+
request.condition_results if request.HasField("condition_results") else None
|
|
75
|
+
)
|
|
76
|
+
context = InvocationContext(
|
|
77
|
+
InvocationMethod.EXECUTE,
|
|
78
|
+
flow,
|
|
79
|
+
request.context,
|
|
80
|
+
self._values,
|
|
81
|
+
request.attributes,
|
|
82
|
+
request.step_exe_locals,
|
|
83
|
+
condition_results,
|
|
84
|
+
)
|
|
85
|
+
input = self._values.decode(request.step_input, step.input_codec)
|
|
86
|
+
decision: Any = step.step.execute(context, input)
|
|
87
|
+
if isawaitable(decision):
|
|
88
|
+
decision = await decision
|
|
89
|
+
if not isinstance(decision, StepDecision):
|
|
90
|
+
raise TypeError("execute must return StepDecision")
|
|
91
|
+
return pb.InvokeExecuteMethodResponse(
|
|
92
|
+
step_decision=self._map_decision(flow, decision),
|
|
93
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
94
|
+
record_events=context.events,
|
|
95
|
+
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
96
|
+
publish_to_channel=context.publications,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def invoke_rpc( # type: ignore[override]
|
|
100
|
+
self,
|
|
101
|
+
original: pb.InvokeWorkerRPCRequest,
|
|
102
|
+
) -> pb.InvokeWorkerRPCResponse:
|
|
103
|
+
request = await self._hydrator.rpc_request(original)
|
|
104
|
+
flow = self._registry._flow_by_type(request.flow_type)
|
|
105
|
+
rpc = flow.rpc(request.rpc_name)
|
|
106
|
+
context = InvocationContext(
|
|
107
|
+
InvocationMethod.RPC,
|
|
108
|
+
flow,
|
|
109
|
+
request.context,
|
|
110
|
+
self._values,
|
|
111
|
+
request.attributes,
|
|
112
|
+
channel_infos=dict(request.channel_infos),
|
|
113
|
+
)
|
|
114
|
+
arguments: list[object] = [context]
|
|
115
|
+
if rpc.input_codec is not None:
|
|
116
|
+
arguments.append(self._values.decode(request.input, rpc.input_codec))
|
|
117
|
+
returned: Any = rpc.method(*arguments)
|
|
118
|
+
if isawaitable(returned):
|
|
119
|
+
returned = await returned
|
|
120
|
+
response = pb.InvokeWorkerRPCResponse(
|
|
121
|
+
upsert_attributes=list(context.attribute_writes.values()),
|
|
122
|
+
record_events=context.events,
|
|
123
|
+
publish_to_channel=context.publications,
|
|
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)
|
|
130
|
+
)
|
|
131
|
+
if returned.next_steps:
|
|
132
|
+
response.step_decision.next_steps.extend(
|
|
133
|
+
self._map_movements(flow, returned.next_steps)
|
|
134
|
+
)
|
|
135
|
+
elif returned is None and rpc.output_codec is None:
|
|
136
|
+
response.output.CopyFrom(self._values.encode_dynamic(None))
|
|
137
|
+
else:
|
|
138
|
+
raise TypeError("RPC must return RPCResult or None")
|
|
139
|
+
return response
|