dex-python-sdk 0.2.0__tar.gz → 0.2.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.2.0 → dex_python_sdk-0.2.2}/PKG-INFO +32 -12
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/README.md +31 -11
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/__init__.py +9 -6
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_async_worker_dispatcher.py +6 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_invocation_context.py +50 -1
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_worker_dispatcher.py +13 -9
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/async_client.py +112 -5
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/async_worker.py +1 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/client.py +112 -5
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/context.py +7 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/dexpb/dex_pb2.py +360 -350
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/dexpb/dex_pb2.pyi +58 -4
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/dexpb/dex_pb2_grpc.py +101 -6
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/flow.py +63 -9
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/flow_config.py +3 -3
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/step.py +15 -15
- dex_python_sdk-0.2.2/dex/stream.py +80 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/worker.py +1 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/pyproject.toml +1 -1
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/LICENSE +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_utils.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/attribute.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/channel.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/condition.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/flow_options.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/wait.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.2}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.0 → dex_python_sdk-0.2.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.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -22,7 +22,7 @@ Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
|
22
22
|
## New user contracts
|
|
23
23
|
|
|
24
24
|
The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts
|
|
25
|
-
from `dex`. This phase includes definitions, attributes, channels, waits,
|
|
25
|
+
from `dex`. This phase includes definitions, attributes, channels, streams, waits,
|
|
26
26
|
decisions, codecs, registry validation, synchronous client calls, and synchronous
|
|
27
27
|
worker handlers. Python owns its gRPC Client and Worker transport;
|
|
28
28
|
the shared Rust Core is used only for BlobCache.
|
|
@@ -34,6 +34,7 @@ import dex
|
|
|
34
34
|
|
|
35
35
|
counter = dex.Attribute("counter", int)
|
|
36
36
|
counters_by_region = dex.AttributeMap("counters-by-region", int)
|
|
37
|
+
progress = dex.Stream("progress", str, 10 * 1024 * 1024)
|
|
37
38
|
|
|
38
39
|
class Run(dex.Step[str]):
|
|
39
40
|
def wait_for(
|
|
@@ -46,6 +47,7 @@ class Run(dex.Step[str]):
|
|
|
46
47
|
def execute(
|
|
47
48
|
self, context: dex.Context, input: str
|
|
48
49
|
) -> dex.StepDecision:
|
|
50
|
+
progress.write(context, "running")
|
|
49
51
|
return dex.graceful_complete(input)
|
|
50
52
|
|
|
51
53
|
class CounterFlow(dex.Flow[str]):
|
|
@@ -58,7 +60,7 @@ class CounterFlow(dex.Flow[str]):
|
|
|
58
60
|
return dex.StepList.start_step(self.run)
|
|
59
61
|
|
|
60
62
|
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
61
|
-
return dex.PersistenceSchema.of(counter, counters_by_region)
|
|
63
|
+
return dex.PersistenceSchema.of(counter, counters_by_region, progress)
|
|
62
64
|
|
|
63
65
|
@dex.rpc(name="Increment")
|
|
64
66
|
def increment(
|
|
@@ -73,9 +75,26 @@ registry = dex.Registry((flow,))
|
|
|
73
75
|
Registry derives codecs from declared Python types and handler annotations.
|
|
74
76
|
Built-in primitive types and dataclasses need no codec arguments. Register an
|
|
75
77
|
explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
76
|
-
`PersistenceSchema.of(...)` accepts attributes and
|
|
78
|
+
`PersistenceSchema.of(...)` accepts attributes, channels, and streams together and
|
|
77
79
|
partitions them by definition type.
|
|
78
80
|
|
|
81
|
+
Streams provide best-effort resumable progress messages. Their approximate byte
|
|
82
|
+
budget is shared by all instances of the owning Flow type. Client keys cannot
|
|
83
|
+
contain `#`; Step writes generate `runID#stepExecutionID` and allow one write per
|
|
84
|
+
Stream per invocation.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.write_stream(flow_id, progress, "frontend/1", "starting")
|
|
88
|
+
message = client.read_stream(
|
|
89
|
+
flow_id, progress, resume_token, timeout=timedelta(seconds=30)
|
|
90
|
+
)
|
|
91
|
+
resume_token = message.resume_token
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Async Step handlers await **Stream.write**, and **AsyncClient** exposes matching
|
|
95
|
+
async methods. Reads return the decoded value, resume token, creation time, and
|
|
96
|
+
idempotency key.
|
|
97
|
+
|
|
79
98
|
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
80
99
|
Dex Server before opening their listener. Existing indexes return immediately;
|
|
81
100
|
failure or the default two-minute deadline aborts startup. An indexed
|
|
@@ -96,13 +115,14 @@ Flow configuration:
|
|
|
96
115
|
|
|
97
116
|
```python
|
|
98
117
|
email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
|
|
99
|
-
config = dex.FlowConfig(
|
|
118
|
+
config = dex.FlowConfig(attribute_store_names=["profiles", "audit"])
|
|
100
119
|
```
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
synchronization while retaining protocol
|
|
121
|
+
Stores are asynchronous latest-state projections. Every enabled Attribute write
|
|
122
|
+
is sent to every selected Store. Deletion writes SQL `NULL`, and projection
|
|
123
|
+
failures do not roll back Flow Attributes. `None` preserves current targets;
|
|
124
|
+
an explicit empty list disables future synchronization while retaining protocol
|
|
125
|
+
presence.
|
|
106
126
|
|
|
107
127
|
```
|
|
108
128
|
pip install dex-python-sdk==0.1.0
|
|
@@ -144,9 +164,9 @@ its normal decision:
|
|
|
144
164
|
|
|
145
165
|
```python
|
|
146
166
|
return (
|
|
147
|
-
dex.go_to(
|
|
148
|
-
.with_canceling_sibling_steps(
|
|
149
|
-
.with_canceling_steps(
|
|
167
|
+
dex.go_to(RecordQuote, quote)
|
|
168
|
+
.with_canceling_sibling_steps(QuoteCarrierA, QuoteCarrierB)
|
|
169
|
+
.with_canceling_steps(GlobalQuoteTimeout)
|
|
150
170
|
)
|
|
151
171
|
```
|
|
152
172
|
|
|
@@ -6,7 +6,7 @@ Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
|
6
6
|
## New user contracts
|
|
7
7
|
|
|
8
8
|
The rewrite targets Python 3.11+ and exposes strongly typed workflow contracts
|
|
9
|
-
from `dex`. This phase includes definitions, attributes, channels, waits,
|
|
9
|
+
from `dex`. This phase includes definitions, attributes, channels, streams, waits,
|
|
10
10
|
decisions, codecs, registry validation, synchronous client calls, and synchronous
|
|
11
11
|
worker handlers. Python owns its gRPC Client and Worker transport;
|
|
12
12
|
the shared Rust Core is used only for BlobCache.
|
|
@@ -18,6 +18,7 @@ import dex
|
|
|
18
18
|
|
|
19
19
|
counter = dex.Attribute("counter", int)
|
|
20
20
|
counters_by_region = dex.AttributeMap("counters-by-region", int)
|
|
21
|
+
progress = dex.Stream("progress", str, 10 * 1024 * 1024)
|
|
21
22
|
|
|
22
23
|
class Run(dex.Step[str]):
|
|
23
24
|
def wait_for(
|
|
@@ -30,6 +31,7 @@ class Run(dex.Step[str]):
|
|
|
30
31
|
def execute(
|
|
31
32
|
self, context: dex.Context, input: str
|
|
32
33
|
) -> dex.StepDecision:
|
|
34
|
+
progress.write(context, "running")
|
|
33
35
|
return dex.graceful_complete(input)
|
|
34
36
|
|
|
35
37
|
class CounterFlow(dex.Flow[str]):
|
|
@@ -42,7 +44,7 @@ class CounterFlow(dex.Flow[str]):
|
|
|
42
44
|
return dex.StepList.start_step(self.run)
|
|
43
45
|
|
|
44
46
|
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
45
|
-
return dex.PersistenceSchema.of(counter, counters_by_region)
|
|
47
|
+
return dex.PersistenceSchema.of(counter, counters_by_region, progress)
|
|
46
48
|
|
|
47
49
|
@dex.rpc(name="Increment")
|
|
48
50
|
def increment(
|
|
@@ -57,9 +59,26 @@ registry = dex.Registry((flow,))
|
|
|
57
59
|
Registry derives codecs from declared Python types and handler annotations.
|
|
58
60
|
Built-in primitive types and dataclasses need no codec arguments. Register an
|
|
59
61
|
explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
60
|
-
`PersistenceSchema.of(...)` accepts attributes and
|
|
62
|
+
`PersistenceSchema.of(...)` accepts attributes, channels, and streams together and
|
|
61
63
|
partitions them by definition type.
|
|
62
64
|
|
|
65
|
+
Streams provide best-effort resumable progress messages. Their approximate byte
|
|
66
|
+
budget is shared by all instances of the owning Flow type. Client keys cannot
|
|
67
|
+
contain `#`; Step writes generate `runID#stepExecutionID` and allow one write per
|
|
68
|
+
Stream per invocation.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
client.write_stream(flow_id, progress, "frontend/1", "starting")
|
|
72
|
+
message = client.read_stream(
|
|
73
|
+
flow_id, progress, resume_token, timeout=timedelta(seconds=30)
|
|
74
|
+
)
|
|
75
|
+
resume_token = message.resume_token
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Async Step handlers await **Stream.write**, and **AsyncClient** exposes matching
|
|
79
|
+
async methods. Reads return the decoded value, resume token, creation time, and
|
|
80
|
+
idempotency key.
|
|
81
|
+
|
|
63
82
|
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
64
83
|
Dex Server before opening their listener. Existing indexes return immediately;
|
|
65
84
|
failure or the default two-minute deadline aborts startup. An indexed
|
|
@@ -80,13 +99,14 @@ Flow configuration:
|
|
|
80
99
|
|
|
81
100
|
```python
|
|
82
101
|
email = dex.Attribute("customer-email", str, sync_to_attribute_store=True)
|
|
83
|
-
config = dex.FlowConfig(
|
|
102
|
+
config = dex.FlowConfig(attribute_store_names=["profiles", "audit"])
|
|
84
103
|
```
|
|
85
104
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
synchronization while retaining protocol
|
|
105
|
+
Stores are asynchronous latest-state projections. Every enabled Attribute write
|
|
106
|
+
is sent to every selected Store. Deletion writes SQL `NULL`, and projection
|
|
107
|
+
failures do not roll back Flow Attributes. `None` preserves current targets;
|
|
108
|
+
an explicit empty list disables future synchronization while retaining protocol
|
|
109
|
+
presence.
|
|
90
110
|
|
|
91
111
|
```
|
|
92
112
|
pip install dex-python-sdk==0.1.0
|
|
@@ -128,9 +148,9 @@ its normal decision:
|
|
|
128
148
|
|
|
129
149
|
```python
|
|
130
150
|
return (
|
|
131
|
-
dex.go_to(
|
|
132
|
-
.with_canceling_sibling_steps(
|
|
133
|
-
.with_canceling_steps(
|
|
151
|
+
dex.go_to(RecordQuote, quote)
|
|
152
|
+
.with_canceling_sibling_steps(QuoteCarrierA, QuoteCarrierB)
|
|
153
|
+
.with_canceling_steps(GlobalQuoteTimeout)
|
|
134
154
|
)
|
|
135
155
|
```
|
|
136
156
|
|
|
@@ -47,14 +47,14 @@ from dex.flow_info import (
|
|
|
47
47
|
from dex.flow_options import (
|
|
48
48
|
FlowTimeoutPolicy,
|
|
49
49
|
IdReusePolicy,
|
|
50
|
-
TimeTravelOptions,
|
|
51
|
-
TimeTravelStepMethod,
|
|
52
|
-
TimeTravelType,
|
|
53
50
|
StartFlowOptions,
|
|
54
|
-
SubFlowOptions,
|
|
55
|
-
SubFlowReusePolicy,
|
|
56
51
|
StopFlowOptions,
|
|
57
52
|
StopType,
|
|
53
|
+
SubFlowOptions,
|
|
54
|
+
SubFlowReusePolicy,
|
|
55
|
+
TimeTravelOptions,
|
|
56
|
+
TimeTravelStepMethod,
|
|
57
|
+
TimeTravelType,
|
|
58
58
|
)
|
|
59
59
|
from dex.flow_result import FlowResult, StepCompletion
|
|
60
60
|
from dex.runtime_errors import (
|
|
@@ -89,8 +89,9 @@ from dex.step import (
|
|
|
89
89
|
graceful_complete,
|
|
90
90
|
)
|
|
91
91
|
from dex.step_execution import StepExecutionId, TimerId
|
|
92
|
-
from dex.
|
|
92
|
+
from dex.stream import Stream, StreamMessage
|
|
93
93
|
from dex.subflow import SubFlow
|
|
94
|
+
from dex.timer import Timer
|
|
94
95
|
from dex.wait import Wait
|
|
95
96
|
from dex.worker import Worker
|
|
96
97
|
from dex.worker_options import WorkerOptions, WorkerTarget
|
|
@@ -159,6 +160,8 @@ __all__ = [
|
|
|
159
160
|
"StepDurability",
|
|
160
161
|
"StepMovement",
|
|
161
162
|
"StepOptions",
|
|
163
|
+
"Stream",
|
|
164
|
+
"StreamMessage",
|
|
162
165
|
"StopFlowOptions",
|
|
163
166
|
"StopType",
|
|
164
167
|
"Timer",
|
|
@@ -28,10 +28,12 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
28
28
|
registry: Registry,
|
|
29
29
|
values: ValueMapper,
|
|
30
30
|
hydrator: AsyncValueHydrator,
|
|
31
|
+
stream_writer: Callable[[pb.WriteStreamRequest], Any],
|
|
31
32
|
) -> None:
|
|
32
33
|
self._registry = registry
|
|
33
34
|
self._values = values
|
|
34
35
|
self._async_hydrator = hydrator
|
|
36
|
+
self._stream_writer = stream_writer
|
|
35
37
|
|
|
36
38
|
async def invoke_wait_for( # type: ignore[override]
|
|
37
39
|
self,
|
|
@@ -46,6 +48,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
46
48
|
flow,
|
|
47
49
|
request.context,
|
|
48
50
|
self._values,
|
|
51
|
+
self._stream_writer,
|
|
49
52
|
request.attributes,
|
|
50
53
|
is_active=is_active,
|
|
51
54
|
)
|
|
@@ -89,6 +92,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
89
92
|
flow,
|
|
90
93
|
request.context,
|
|
91
94
|
self._values,
|
|
95
|
+
self._stream_writer,
|
|
92
96
|
request.attributes,
|
|
93
97
|
request.step_exe_locals,
|
|
94
98
|
condition_results,
|
|
@@ -137,6 +141,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
137
141
|
flow,
|
|
138
142
|
request.context,
|
|
139
143
|
self._values,
|
|
144
|
+
self._stream_writer,
|
|
140
145
|
request.attributes,
|
|
141
146
|
request.step_exe_locals,
|
|
142
147
|
condition_results,
|
|
@@ -172,6 +177,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
172
177
|
flow,
|
|
173
178
|
request.context,
|
|
174
179
|
self._values,
|
|
180
|
+
self._stream_writer,
|
|
175
181
|
request.attributes,
|
|
176
182
|
channel_infos=dict(request.channel_infos),
|
|
177
183
|
is_active=is_active,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
from enum import Enum
|
|
12
|
+
from inspect import isawaitable
|
|
12
13
|
from typing import Any, Callable, Sequence, TypeVar, cast
|
|
13
14
|
from urllib.parse import unquote
|
|
14
15
|
|
|
@@ -20,9 +21,12 @@ from dex.codec import Codec
|
|
|
20
21
|
from dex.dexpb import dex_pb2 as pb
|
|
21
22
|
from dex.flow import Registry, _RegisteredFlow
|
|
22
23
|
from dex.flow_result import FlowResult, flow_result_from_proto
|
|
24
|
+
from dex.stream import Stream
|
|
23
25
|
|
|
24
26
|
ValueT = TypeVar("ValueT")
|
|
25
|
-
_Definition =
|
|
27
|
+
_Definition = (
|
|
28
|
+
Attribute[Any] | AttributeMap[Any] | Channel[Any] | ChannelMap[Any] | Stream[Any]
|
|
29
|
+
)
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class InvocationMethod(Enum):
|
|
@@ -38,6 +42,7 @@ class InvocationContext:
|
|
|
38
42
|
flow: _RegisteredFlow,
|
|
39
43
|
metadata: pb.Context,
|
|
40
44
|
values: ValueMapper,
|
|
45
|
+
stream_writer: Callable[[pb.WriteStreamRequest], Any],
|
|
41
46
|
attributes: Sequence[pb.KV],
|
|
42
47
|
locals: Sequence[pb.KV] = (),
|
|
43
48
|
condition_results: pb.ConditionResults | None = None,
|
|
@@ -48,6 +53,7 @@ class InvocationContext:
|
|
|
48
53
|
self._flow = flow
|
|
49
54
|
self._metadata = metadata
|
|
50
55
|
self._values = values
|
|
56
|
+
self._stream_writer = stream_writer
|
|
51
57
|
self._attributes = self._map_values("Attribute", attributes)
|
|
52
58
|
self._locals = self._map_values("step-execution local", locals)
|
|
53
59
|
self._condition_results = condition_results
|
|
@@ -58,6 +64,7 @@ class InvocationContext:
|
|
|
58
64
|
self.events: list[pb.KV] = []
|
|
59
65
|
self.publications: list[pb.ChannelMessage] = []
|
|
60
66
|
self._event_names: set[str] = set()
|
|
67
|
+
self._stream_writes: set[int] = set()
|
|
61
68
|
|
|
62
69
|
@property
|
|
63
70
|
def flow_id(self) -> str:
|
|
@@ -151,6 +158,48 @@ class InvocationContext:
|
|
|
151
158
|
self._event_names.add(name)
|
|
152
159
|
self.events.append(pb.KV(key=name, value=self._values.encode_dynamic(value)))
|
|
153
160
|
|
|
161
|
+
def _write_stream(
|
|
162
|
+
self,
|
|
163
|
+
definition: Stream[ValueT],
|
|
164
|
+
value: ValueT,
|
|
165
|
+
) -> Any:
|
|
166
|
+
if self._method is InvocationMethod.RPC:
|
|
167
|
+
raise ValueError("Stream writes require a Step Context")
|
|
168
|
+
self._require_registered(definition)
|
|
169
|
+
identity = id(definition)
|
|
170
|
+
if identity in self._stream_writes:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Stream {definition.name} was already written by this Step execution"
|
|
173
|
+
)
|
|
174
|
+
request = pb.WriteStreamRequest(
|
|
175
|
+
flow_id=self.flow_id,
|
|
176
|
+
flow_type=self._flow.name,
|
|
177
|
+
stream_name=definition.name,
|
|
178
|
+
max_estimated_bytes=definition.max_estimated_bytes,
|
|
179
|
+
value=self._values.encode(
|
|
180
|
+
value,
|
|
181
|
+
self._values.codec(definition.value_type),
|
|
182
|
+
),
|
|
183
|
+
idempotency_key=f"{self.run_id}#{self.step_execution_id}",
|
|
184
|
+
)
|
|
185
|
+
result = self._stream_writer(request)
|
|
186
|
+
if isawaitable(result):
|
|
187
|
+
self._stream_writes.add(identity)
|
|
188
|
+
return self._finish_async_stream_write(result, identity)
|
|
189
|
+
self._stream_writes.add(identity)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
async def _finish_async_stream_write(
|
|
193
|
+
self,
|
|
194
|
+
result: Any,
|
|
195
|
+
identity: int,
|
|
196
|
+
) -> None:
|
|
197
|
+
try:
|
|
198
|
+
await result
|
|
199
|
+
except BaseException:
|
|
200
|
+
self._stream_writes.remove(identity)
|
|
201
|
+
raise
|
|
202
|
+
|
|
154
203
|
def _get_attribute(
|
|
155
204
|
self,
|
|
156
205
|
definition: Attribute[ValueT] | AttributeMap[ValueT],
|
|
@@ -49,10 +49,12 @@ class WorkerDispatcher:
|
|
|
49
49
|
registry: Registry,
|
|
50
50
|
values: ValueMapper,
|
|
51
51
|
hydrator: ValueHydrator,
|
|
52
|
+
stream_writer: Callable[[pb.WriteStreamRequest], Any],
|
|
52
53
|
) -> None:
|
|
53
54
|
self._registry = registry
|
|
54
55
|
self._values = values
|
|
55
56
|
self._hydrator = hydrator
|
|
57
|
+
self._stream_writer = stream_writer
|
|
56
58
|
|
|
57
59
|
def invoke_wait_for(
|
|
58
60
|
self,
|
|
@@ -67,6 +69,7 @@ class WorkerDispatcher:
|
|
|
67
69
|
flow,
|
|
68
70
|
request.context,
|
|
69
71
|
self._values,
|
|
72
|
+
self._stream_writer,
|
|
70
73
|
request.attributes,
|
|
71
74
|
is_active=is_active,
|
|
72
75
|
)
|
|
@@ -112,6 +115,7 @@ class WorkerDispatcher:
|
|
|
112
115
|
flow,
|
|
113
116
|
request.context,
|
|
114
117
|
self._values,
|
|
118
|
+
self._stream_writer,
|
|
115
119
|
request.attributes,
|
|
116
120
|
request.step_exe_locals,
|
|
117
121
|
condition_results,
|
|
@@ -162,6 +166,7 @@ class WorkerDispatcher:
|
|
|
162
166
|
flow,
|
|
163
167
|
request.context,
|
|
164
168
|
self._values,
|
|
169
|
+
self._stream_writer,
|
|
165
170
|
request.attributes,
|
|
166
171
|
request.step_exe_locals,
|
|
167
172
|
condition_results,
|
|
@@ -199,6 +204,7 @@ class WorkerDispatcher:
|
|
|
199
204
|
flow,
|
|
200
205
|
request.context,
|
|
201
206
|
self._values,
|
|
207
|
+
self._stream_writer,
|
|
202
208
|
request.attributes,
|
|
203
209
|
channel_infos=dict(request.channel_infos),
|
|
204
210
|
is_active=is_active,
|
|
@@ -456,8 +462,10 @@ class WorkerDispatcher:
|
|
|
456
462
|
is_headless_address=config.worker_target.headless,
|
|
457
463
|
)
|
|
458
464
|
)
|
|
459
|
-
if config.
|
|
460
|
-
mapped.
|
|
465
|
+
if config.attribute_store_names is not None:
|
|
466
|
+
mapped.attribute_store_names.CopyFrom(
|
|
467
|
+
pb.AttributeStoreNames(names=config.attribute_store_names)
|
|
468
|
+
)
|
|
461
469
|
return mapped
|
|
462
470
|
|
|
463
471
|
def _map_decision(
|
|
@@ -529,7 +537,7 @@ class WorkerDispatcher:
|
|
|
529
537
|
def _map_cancellation_steps(
|
|
530
538
|
self,
|
|
531
539
|
flow: _RegisteredFlow,
|
|
532
|
-
steps: tuple[Step[Any], ...],
|
|
540
|
+
steps: tuple[type[Step[Any]], ...],
|
|
533
541
|
) -> list[str]:
|
|
534
542
|
step_types: list[str] = []
|
|
535
543
|
seen: set[str] = set()
|
|
@@ -585,13 +593,9 @@ class WorkerDispatcher:
|
|
|
585
593
|
@staticmethod
|
|
586
594
|
def _registered_movement_target(
|
|
587
595
|
flow: _RegisteredFlow,
|
|
588
|
-
step:
|
|
596
|
+
step: type[Step[Any]],
|
|
589
597
|
) -> _RegisteredStep:
|
|
590
|
-
|
|
591
|
-
target = flow.step(step_type)
|
|
592
|
-
if target.step is not step:
|
|
593
|
-
raise ValueError("Step movement target does not belong to Flow")
|
|
594
|
-
return target
|
|
598
|
+
return flow.step_class(step)
|
|
595
599
|
|
|
596
600
|
@staticmethod
|
|
597
601
|
def _require_persistence_identity(
|
|
@@ -35,18 +35,19 @@ from dex.flow_info import FlowInfo, FlowStatus, SearchFlowEntry, SearchFlowsPage
|
|
|
35
35
|
from dex.flow_options import (
|
|
36
36
|
FlowTimeoutPolicy,
|
|
37
37
|
IdReusePolicy,
|
|
38
|
-
TimeTravelOptions,
|
|
39
|
-
TimeTravelStepMethod,
|
|
40
|
-
TimeTravelType,
|
|
41
38
|
StartFlowOptions,
|
|
42
39
|
StopFlowOptions,
|
|
43
40
|
StopType,
|
|
41
|
+
TimeTravelOptions,
|
|
42
|
+
TimeTravelStepMethod,
|
|
43
|
+
TimeTravelType,
|
|
44
44
|
_resolve_flow_timeout_policy,
|
|
45
45
|
)
|
|
46
46
|
from dex.flow_result import FlowResult, flow_result_from_proto
|
|
47
47
|
from dex.runtime_errors import FlowErrorType
|
|
48
48
|
from dex.step import RetryPolicy, StepDurability
|
|
49
49
|
from dex.step_execution import StepExecutionId, TimerId
|
|
50
|
+
from dex.stream import Stream, StreamMessage
|
|
50
51
|
|
|
51
52
|
InputT = TypeVar("InputT")
|
|
52
53
|
OutputT = TypeVar("OutputT")
|
|
@@ -97,6 +98,7 @@ class AsyncClient:
|
|
|
97
98
|
registry,
|
|
98
99
|
self._values,
|
|
99
100
|
cast(Any, self._hydrator),
|
|
101
|
+
self._service.WriteStream,
|
|
100
102
|
)
|
|
101
103
|
self._closed = False
|
|
102
104
|
|
|
@@ -502,6 +504,109 @@ class AsyncClient:
|
|
|
502
504
|
"active",
|
|
503
505
|
)
|
|
504
506
|
|
|
507
|
+
async def write_stream(
|
|
508
|
+
self,
|
|
509
|
+
flow_id: str,
|
|
510
|
+
stream: Stream[ValueT],
|
|
511
|
+
idempotency_key: str,
|
|
512
|
+
value: ValueT,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Await one typed best-effort Stream append with client idempotency.
|
|
515
|
+
|
|
516
|
+
Args:
|
|
517
|
+
flow_id: Logical Flow instance ID; the Flow need not exist or be active.
|
|
518
|
+
stream: Exact Stream object registered in one Flow schema.
|
|
519
|
+
idempotency_key: Non-empty key without ``#``; retained duplicates are no-ops.
|
|
520
|
+
value: Typed message to append.
|
|
521
|
+
|
|
522
|
+
Raises:
|
|
523
|
+
ValueError: If an ID is empty or the key contains the reserved separator.
|
|
524
|
+
FlowDefinitionError: If the Stream is not registered.
|
|
525
|
+
ValueMappingError: If the message cannot be encoded.
|
|
526
|
+
DexServiceError: If FlowService cannot append the message.
|
|
527
|
+
"""
|
|
528
|
+
require_name(idempotency_key)
|
|
529
|
+
if "#" in idempotency_key:
|
|
530
|
+
raise ValueError("Stream client idempotency key must not contain #")
|
|
531
|
+
flow = self.registry._flow_for_stream(stream)
|
|
532
|
+
await self._call(
|
|
533
|
+
self._service.WriteStream,
|
|
534
|
+
pb.WriteStreamRequest(
|
|
535
|
+
flow_id=require_name(flow_id),
|
|
536
|
+
flow_type=flow.name,
|
|
537
|
+
stream_name=stream.name,
|
|
538
|
+
max_estimated_bytes=stream.max_estimated_bytes,
|
|
539
|
+
value=self._values.encode(
|
|
540
|
+
value,
|
|
541
|
+
self._values.codec(stream.value_type),
|
|
542
|
+
),
|
|
543
|
+
idempotency_key=idempotency_key,
|
|
544
|
+
),
|
|
545
|
+
"write_stream",
|
|
546
|
+
flow_id,
|
|
547
|
+
"none",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
async def read_stream(
|
|
551
|
+
self,
|
|
552
|
+
flow_id: str,
|
|
553
|
+
stream: Stream[ValueT],
|
|
554
|
+
resume_token: str = "",
|
|
555
|
+
timeout: timedelta | None = None,
|
|
556
|
+
) -> StreamMessage[ValueT]:
|
|
557
|
+
"""Await the next retained Stream message after a resume token.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
flow_id: Logical Flow instance ID used as the Stream instance key.
|
|
561
|
+
stream: Exact Stream object registered in one Flow schema.
|
|
562
|
+
resume_token: Previous message token, or empty for the retained head.
|
|
563
|
+
timeout: Optional non-negative server-side long-poll duration.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The decoded message, next resume token, creation time, and idempotency key.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
LongPollTimeoutError: If no message arrives before the server wait expires.
|
|
570
|
+
FlowDefinitionError: If the Stream is not registered.
|
|
571
|
+
ValueMappingError: If the retained message cannot be decoded.
|
|
572
|
+
DexServiceError: If FlowService cannot perform the read.
|
|
573
|
+
"""
|
|
574
|
+
flow = self.registry._flow_for_stream(stream)
|
|
575
|
+
response = cast(
|
|
576
|
+
pb.ReadStreamResponse,
|
|
577
|
+
await self._call(
|
|
578
|
+
self._service.ReadStream,
|
|
579
|
+
pb.ReadStreamRequest(
|
|
580
|
+
flow_id=require_name(flow_id),
|
|
581
|
+
flow_type=flow.name,
|
|
582
|
+
stream_name=stream.name,
|
|
583
|
+
resume_token=resume_token,
|
|
584
|
+
wait_time_seconds=(
|
|
585
|
+
0 if timeout is None else self._seconds32(timeout)
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
"read_stream",
|
|
589
|
+
flow_id,
|
|
590
|
+
"none",
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
if (
|
|
594
|
+
not response.HasField("message")
|
|
595
|
+
or not response.message.HasField("value")
|
|
596
|
+
or not response.message.HasField("created_time")
|
|
597
|
+
or not response.message.resume_token
|
|
598
|
+
):
|
|
599
|
+
raise ValueError("Dex returned an incomplete Stream message")
|
|
600
|
+
return StreamMessage(
|
|
601
|
+
self._values.decode(
|
|
602
|
+
response.message.value,
|
|
603
|
+
self._values.codec(stream.value_type),
|
|
604
|
+
),
|
|
605
|
+
response.message.resume_token,
|
|
606
|
+
response.message.created_time.ToDatetime(tzinfo=timezone.utc),
|
|
607
|
+
response.message.idempotency_key,
|
|
608
|
+
)
|
|
609
|
+
|
|
505
610
|
async def wait_for_flow(
|
|
506
611
|
self,
|
|
507
612
|
flow_id: str,
|
|
@@ -1077,8 +1182,10 @@ class AsyncClient:
|
|
|
1077
1182
|
StepDurability.SYNC: pb.STEP_DURABILITY_SYNC,
|
|
1078
1183
|
StepDurability.ASYNC: pb.STEP_DURABILITY_ASYNC,
|
|
1079
1184
|
}[config.step_durability]
|
|
1080
|
-
if config.
|
|
1081
|
-
mapped.
|
|
1185
|
+
if config.attribute_store_names is not None:
|
|
1186
|
+
mapped.attribute_store_names.CopyFrom(
|
|
1187
|
+
pb.AttributeStoreNames(names=config.attribute_store_names)
|
|
1188
|
+
)
|
|
1082
1189
|
target = (
|
|
1083
1190
|
config.worker_target
|
|
1084
1191
|
if config is not None and config.worker_target is not None
|