dex-python-sdk 0.2.8__tar.gz → 0.2.10__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.8 → dex_python_sdk-0.2.10}/PKG-INFO +49 -5
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/README.md +48 -4
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/__init__.py +9 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_async_value_hydrator.py +16 -2
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_async_worker_dispatcher.py +5 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_grpc_errors.py +6 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_invocation_context.py +104 -4
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_utils.py +14 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_value_hydrator.py +16 -2
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_worker_dispatcher.py +5 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/async_client.py +149 -3
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/attribute.py +60 -12
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/channel.py +201 -18
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/client.py +147 -3
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/condition.py +3 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/context.py +14 -1
- dex_python_sdk-0.2.10/dex/dexpb/dex_pb2.py +477 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/dexpb/dex_pb2.pyi +126 -24
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/dexpb/dex_pb2_grpc.py +95 -9
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/flow.py +127 -11
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/flow_options.py +5 -5
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/runtime_errors.py +14 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/wait.py +18 -2
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/pyproject.toml +1 -1
- dex_python_sdk-0.2.8/dex/dexpb/dex_pb2.py +0 -459
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/step.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/stream.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/worker.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.10}/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.10
|
|
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
|
|
@@ -17,6 +17,47 @@ Project-URL: Repository, https://github.com/superdurable/dex
|
|
|
17
17
|
|
|
18
18
|
# Dex SDK for Python
|
|
19
19
|
|
|
20
|
+
## Pending Channel messages
|
|
21
|
+
|
|
22
|
+
`Client.get_channel_messages(flow_id, channel)` returns typed `ChannelMessage`
|
|
23
|
+
envelopes in FIFO order. Each envelope contains the decoded value and the UUIDv7
|
|
24
|
+
assigned by Dex. `Client.delete_channel_message` deletes only a still-pending
|
|
25
|
+
message and raises `ChannelMessageNotFoundError` after consumption or another
|
|
26
|
+
deletion.
|
|
27
|
+
|
|
28
|
+
RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
|
|
29
|
+
`@rpc(is_transactional=True)` when a missing message must abort its other writes.
|
|
30
|
+
Attribute locks already select transactional execution, but Channel deletion
|
|
31
|
+
itself does not.
|
|
32
|
+
|
|
33
|
+
## RPC state loading
|
|
34
|
+
|
|
35
|
+
RPCs receive ordinary Attributes and all Channel size metadata by default.
|
|
36
|
+
AttributeMap entries and pending Channel messages are opt-in:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
@dex.rpc(
|
|
40
|
+
load_attribute_map_instances=(items.load("tenant-a"),),
|
|
41
|
+
load_channels=(queued,),
|
|
42
|
+
load_channel_maps=(by_tenant,),
|
|
43
|
+
)
|
|
44
|
+
def snapshot(self, context: dex.Context) -> dex.RPCResult[Snapshot]:
|
|
45
|
+
messages = queued.pending_messages(context)
|
|
46
|
+
return dex.RPCResult(Snapshot(messages=messages))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Put an AttributeMap or ChannelMap directly in its plural load option when every
|
|
50
|
+
current instance is needed. Use the singular instance options for the less common
|
|
51
|
+
exact-instance case. A selected empty queue returns an empty tuple; reading an
|
|
52
|
+
unselected map entry or pending-message snapshot raises **StateNotLoadedError**.
|
|
53
|
+
Pending messages preserve FIFO order and include the server-assigned message ID.
|
|
54
|
+
The snapshot does not change after the handler stages a publish or deletion.
|
|
55
|
+
|
|
56
|
+
Loading controls which data reaches the Worker. **is_transactional** controls
|
|
57
|
+
atomic commit and Channel deletion validation. Attribute locks add isolation
|
|
58
|
+
only among cooperating Steps and RPCs using the same lock. Write-only publish
|
|
59
|
+
and delete operations do not require loading.
|
|
60
|
+
|
|
20
61
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
21
62
|
|
|
22
63
|
## New user contracts
|
|
@@ -271,14 +312,17 @@ instead of raw Flow, Step, or RPC strings.
|
|
|
271
312
|
|
|
272
313
|
### Waiting and map inspection
|
|
273
314
|
|
|
274
|
-
`Wait.all_of
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
`Wait.until`, `Wait.all_of`, and `Wait.any_of` use unnamed Conditions by
|
|
316
|
+
default. Do not add condition IDs merely because a Condition is nested in one
|
|
317
|
+
of these waits. Every Condition in `Wait.any_combination_of` must have a
|
|
318
|
+
non-empty user ID; the same Condition instance may appear in multiple
|
|
319
|
+
combinations.
|
|
277
320
|
|
|
278
321
|
Both `Client` and `AsyncClient` provide singleton and AttributeMap-instance
|
|
279
322
|
overloads of `wait_for_attribute_equal`. They target the current run and accept
|
|
280
323
|
only string, bool, int, or float wire values. JSON objects, bytes, and null fail
|
|
281
|
-
before transport.
|
|
324
|
+
before transport. Every AttributeMap and ChannelMap instance must be non-empty
|
|
325
|
+
and must not contain `/`. `AttributeMap.get_map_size/get_all_instance_keys` include
|
|
282
326
|
buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
|
|
283
327
|
include buffered publishes, and omit empty instances. Keys are decoded and
|
|
284
328
|
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
|
|
2
2
|
# Dex SDK for Python
|
|
3
3
|
|
|
4
|
+
## Pending Channel messages
|
|
5
|
+
|
|
6
|
+
`Client.get_channel_messages(flow_id, channel)` returns typed `ChannelMessage`
|
|
7
|
+
envelopes in FIFO order. Each envelope contains the decoded value and the UUIDv7
|
|
8
|
+
assigned by Dex. `Client.delete_channel_message` deletes only a still-pending
|
|
9
|
+
message and raises `ChannelMessageNotFoundError` after consumption or another
|
|
10
|
+
deletion.
|
|
11
|
+
|
|
12
|
+
RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
|
|
13
|
+
`@rpc(is_transactional=True)` when a missing message must abort its other writes.
|
|
14
|
+
Attribute locks already select transactional execution, but Channel deletion
|
|
15
|
+
itself does not.
|
|
16
|
+
|
|
17
|
+
## RPC state loading
|
|
18
|
+
|
|
19
|
+
RPCs receive ordinary Attributes and all Channel size metadata by default.
|
|
20
|
+
AttributeMap entries and pending Channel messages are opt-in:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
@dex.rpc(
|
|
24
|
+
load_attribute_map_instances=(items.load("tenant-a"),),
|
|
25
|
+
load_channels=(queued,),
|
|
26
|
+
load_channel_maps=(by_tenant,),
|
|
27
|
+
)
|
|
28
|
+
def snapshot(self, context: dex.Context) -> dex.RPCResult[Snapshot]:
|
|
29
|
+
messages = queued.pending_messages(context)
|
|
30
|
+
return dex.RPCResult(Snapshot(messages=messages))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Put an AttributeMap or ChannelMap directly in its plural load option when every
|
|
34
|
+
current instance is needed. Use the singular instance options for the less common
|
|
35
|
+
exact-instance case. A selected empty queue returns an empty tuple; reading an
|
|
36
|
+
unselected map entry or pending-message snapshot raises **StateNotLoadedError**.
|
|
37
|
+
Pending messages preserve FIFO order and include the server-assigned message ID.
|
|
38
|
+
The snapshot does not change after the handler stages a publish or deletion.
|
|
39
|
+
|
|
40
|
+
Loading controls which data reaches the Worker. **is_transactional** controls
|
|
41
|
+
atomic commit and Channel deletion validation. Attribute locks add isolation
|
|
42
|
+
only among cooperating Steps and RPCs using the same lock. Write-only publish
|
|
43
|
+
and delete operations do not require loading.
|
|
44
|
+
|
|
4
45
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
5
46
|
|
|
6
47
|
## New user contracts
|
|
@@ -255,14 +296,17 @@ instead of raw Flow, Step, or RPC strings.
|
|
|
255
296
|
|
|
256
297
|
### Waiting and map inspection
|
|
257
298
|
|
|
258
|
-
`Wait.all_of
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
`Wait.until`, `Wait.all_of`, and `Wait.any_of` use unnamed Conditions by
|
|
300
|
+
default. Do not add condition IDs merely because a Condition is nested in one
|
|
301
|
+
of these waits. Every Condition in `Wait.any_combination_of` must have a
|
|
302
|
+
non-empty user ID; the same Condition instance may appear in multiple
|
|
303
|
+
combinations.
|
|
261
304
|
|
|
262
305
|
Both `Client` and `AsyncClient` provide singleton and AttributeMap-instance
|
|
263
306
|
overloads of `wait_for_attribute_equal`. They target the current run and accept
|
|
264
307
|
only string, bool, int, or float wire values. JSON objects, bytes, and null fail
|
|
265
|
-
before transport.
|
|
308
|
+
before transport. Every AttributeMap and ChannelMap instance must be non-empty
|
|
309
|
+
and must not contain `/`. `AttributeMap.get_map_size/get_all_instance_keys` include
|
|
266
310
|
buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
|
|
267
311
|
include buffered publishes, and omit empty instances. Keys are decoded and
|
|
268
312
|
sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
|
|
@@ -16,10 +16,11 @@ from dex.attribute import (
|
|
|
16
16
|
AttributeIndex,
|
|
17
17
|
AttributeLock,
|
|
18
18
|
AttributeMap,
|
|
19
|
+
AttributeMapLoad,
|
|
19
20
|
IndexType,
|
|
20
21
|
)
|
|
21
22
|
from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
|
|
22
|
-
from dex.channel import Channel, ChannelMap
|
|
23
|
+
from dex.channel import Channel, ChannelMap, ChannelMapLoad, ChannelMessage
|
|
23
24
|
from dex.client import Client
|
|
24
25
|
from dex.client_options import ClientOptions
|
|
25
26
|
from dex.codec import (
|
|
@@ -68,7 +69,9 @@ from dex.runtime_errors import (
|
|
|
68
69
|
FlowNotFoundError,
|
|
69
70
|
InvalidStepResultError,
|
|
70
71
|
LongPollTimeoutError,
|
|
72
|
+
ChannelMessageNotFoundError,
|
|
71
73
|
RpcLockConflictError,
|
|
74
|
+
StateNotLoadedError,
|
|
72
75
|
ValueMappingError,
|
|
73
76
|
WorkerInvocationError,
|
|
74
77
|
)
|
|
@@ -115,6 +118,7 @@ __all__ = [
|
|
|
115
118
|
"AttributeIndex",
|
|
116
119
|
"AttributeLock",
|
|
117
120
|
"AttributeMap",
|
|
121
|
+
"AttributeMapLoad",
|
|
118
122
|
"AsyncContext",
|
|
119
123
|
"AsyncClient",
|
|
120
124
|
"AsyncBufferedTextStream",
|
|
@@ -124,6 +128,9 @@ __all__ = [
|
|
|
124
128
|
"BufferedTextStream",
|
|
125
129
|
"Channel",
|
|
126
130
|
"ChannelMap",
|
|
131
|
+
"ChannelMapLoad",
|
|
132
|
+
"ChannelMessage",
|
|
133
|
+
"ChannelMessageNotFoundError",
|
|
127
134
|
"Client",
|
|
128
135
|
"ClientOptions",
|
|
129
136
|
"Codec",
|
|
@@ -153,6 +160,7 @@ __all__ = [
|
|
|
153
160
|
"RPCResult",
|
|
154
161
|
"RetryAfterError",
|
|
155
162
|
"RpcLockConflictError",
|
|
163
|
+
"StateNotLoadedError",
|
|
156
164
|
"Registry",
|
|
157
165
|
"TimeTravelOptions",
|
|
158
166
|
"TimeTravelStepMethod",
|
|
@@ -124,10 +124,24 @@ class AsyncValueHydrator:
|
|
|
124
124
|
result = pb.InvokeWorkerRPCRequest()
|
|
125
125
|
result.CopyFrom(request)
|
|
126
126
|
values = [request.input, *(entry.value for entry in request.attributes)]
|
|
127
|
+
channel_messages = [
|
|
128
|
+
message
|
|
129
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
130
|
+
for message in channel_values.messages
|
|
131
|
+
]
|
|
132
|
+
values.extend(message.value for message in channel_messages)
|
|
127
133
|
hydrated = await self.hydrate_all(values)
|
|
128
134
|
result.input.CopyFrom(hydrated[0])
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
hydrated_entries = iter(hydrated[1:])
|
|
136
|
+
for entry in result.attributes:
|
|
137
|
+
entry.value.CopyFrom(next(hydrated_entries))
|
|
138
|
+
result_messages = [
|
|
139
|
+
message
|
|
140
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
141
|
+
for message in channel_values.messages
|
|
142
|
+
]
|
|
143
|
+
for message in result_messages:
|
|
144
|
+
message.value.CopyFrom(next(hydrated_entries))
|
|
131
145
|
return result
|
|
132
146
|
|
|
133
147
|
async def step_outputs(
|
|
@@ -294,6 +294,10 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
294
294
|
self._values,
|
|
295
295
|
request.attributes,
|
|
296
296
|
channel_infos=dict(request.channel_infos),
|
|
297
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
298
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
299
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
300
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
297
301
|
is_active=is_active,
|
|
298
302
|
)
|
|
299
303
|
arguments: list[object] = [context]
|
|
@@ -306,6 +310,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
306
310
|
response = pb.InvokeWorkerRPCResponse(
|
|
307
311
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
308
312
|
record_events=context.events,
|
|
313
|
+
delete_from_channel=context.channel_deletions,
|
|
309
314
|
publish_to_channel=context.publications,
|
|
310
315
|
)
|
|
311
316
|
if isinstance(returned, RPCResult):
|
|
@@ -19,6 +19,7 @@ from grpc_status import rpc_status
|
|
|
19
19
|
|
|
20
20
|
from dex.dexpb import dex_pb2 as pb
|
|
21
21
|
from dex.runtime_errors import (
|
|
22
|
+
ChannelMessageNotFoundError,
|
|
22
23
|
DexServiceError,
|
|
23
24
|
ErrorSubStatus,
|
|
24
25
|
FlowAlreadyStartedError,
|
|
@@ -136,6 +137,8 @@ def translate_rpc_error(
|
|
|
136
137
|
)
|
|
137
138
|
if sub_status is ErrorSubStatus.LONG_POLL_TIMEOUT:
|
|
138
139
|
return LongPollTimeoutError(*parameters)
|
|
140
|
+
if sub_status is ErrorSubStatus.CHANNEL_MESSAGE_NOT_FOUND:
|
|
141
|
+
return ChannelMessageNotFoundError(*parameters)
|
|
139
142
|
return DexServiceError(*parameters)
|
|
140
143
|
|
|
141
144
|
|
|
@@ -219,5 +222,8 @@ def _map_sub_status(value: int) -> ErrorSubStatus:
|
|
|
219
222
|
int(pb.ERROR_SUB_STATUS_FLOW_NOT_EXISTS): ErrorSubStatus.FLOW_NOT_EXISTS,
|
|
220
223
|
int(pb.ERROR_SUB_STATUS_WORKER_API_ERROR): ErrorSubStatus.WORKER_API_ERROR,
|
|
221
224
|
int(pb.ERROR_SUB_STATUS_LONG_POLL_TIME_OUT): ErrorSubStatus.LONG_POLL_TIMEOUT,
|
|
225
|
+
int(
|
|
226
|
+
pb.ERROR_SUB_STATUS_CHANNEL_MESSAGE_NOT_FOUND
|
|
227
|
+
): ErrorSubStatus.CHANNEL_MESSAGE_NOT_FOUND,
|
|
222
228
|
}
|
|
223
229
|
return statuses.get(value, ErrorSubStatus.UNCATEGORIZED)
|
|
@@ -12,14 +12,15 @@ from enum import Enum
|
|
|
12
12
|
from typing import Any, Callable, Protocol, Sequence, TypeVar, cast
|
|
13
13
|
from urllib.parse import unquote
|
|
14
14
|
|
|
15
|
-
from dex._utils import require_name
|
|
15
|
+
from dex._utils import require_map_instance, require_name
|
|
16
16
|
from dex._value_mapper import ValueMapper
|
|
17
17
|
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
18
|
-
from dex.channel import Channel, ChannelMap
|
|
18
|
+
from dex.channel import Channel, ChannelMap, ChannelMessage
|
|
19
19
|
from dex.codec import Codec
|
|
20
20
|
from dex.dexpb import dex_pb2 as pb
|
|
21
21
|
from dex.flow import Registry, _RegisteredFlow
|
|
22
22
|
from dex.flow_result import FlowResult, flow_result_from_proto
|
|
23
|
+
from dex.runtime_errors import StateNotLoadedError
|
|
23
24
|
from dex.step import (
|
|
24
25
|
_NO_HEARTBEAT_VALUE,
|
|
25
26
|
StepOutput,
|
|
@@ -66,6 +67,10 @@ class InvocationContext:
|
|
|
66
67
|
locals: Sequence[pb.KV] = (),
|
|
67
68
|
condition_results: pb.ConditionResults | None = None,
|
|
68
69
|
channel_infos: dict[str, pb.ChannelInfo] | None = None,
|
|
70
|
+
loaded_channel_messages: dict[str, pb.ChannelValues] | None = None,
|
|
71
|
+
loaded_attribute_map_instances: Sequence[str] = (),
|
|
72
|
+
loaded_channel_names: Sequence[str] = (),
|
|
73
|
+
loaded_channel_map_instances: Sequence[str] = (),
|
|
69
74
|
is_active: Callable[[], bool] | None = None,
|
|
70
75
|
output_emitter: _StepOutputEmitter | None = None,
|
|
71
76
|
) -> None:
|
|
@@ -77,11 +82,16 @@ class InvocationContext:
|
|
|
77
82
|
self._locals = self._map_values("step-execution local", locals)
|
|
78
83
|
self._condition_results = condition_results
|
|
79
84
|
self._channel_infos = dict(channel_infos or {})
|
|
85
|
+
self._loaded_channel_messages = dict(loaded_channel_messages or {})
|
|
86
|
+
self._loaded_attribute_map_instances = frozenset(loaded_attribute_map_instances)
|
|
87
|
+
self._loaded_channel_names = frozenset(loaded_channel_names)
|
|
88
|
+
self._loaded_channel_map_instances = frozenset(loaded_channel_map_instances)
|
|
80
89
|
self._is_active = is_active or _always_active
|
|
81
90
|
self._output_emitter = output_emitter
|
|
82
91
|
self.attribute_writes: dict[str, pb.AttributeWrite] = {}
|
|
83
92
|
self.local_writes: dict[str, pb.KV] = {}
|
|
84
93
|
self.events: list[pb.KV] = []
|
|
94
|
+
self.channel_deletions: list[pb.ChannelMessageDeletion] = []
|
|
85
95
|
self.publications: list[pb.ChannelMessage] = []
|
|
86
96
|
self._event_names: set[str] = set()
|
|
87
97
|
self._step_output_finalizers: list[_StepOutputFinalizer] = []
|
|
@@ -275,6 +285,7 @@ class InvocationContext:
|
|
|
275
285
|
instance: str | None,
|
|
276
286
|
) -> ValueT:
|
|
277
287
|
self._require_registered(definition)
|
|
288
|
+
self._require_attribute_map_instance_loaded(definition, instance)
|
|
278
289
|
key = self._physical_name(definition, instance)
|
|
279
290
|
write = self.attribute_writes.get(key)
|
|
280
291
|
if write is not None:
|
|
@@ -349,11 +360,32 @@ class InvocationContext:
|
|
|
349
360
|
size=(current.size if current is not None else 0) + 1
|
|
350
361
|
)
|
|
351
362
|
|
|
363
|
+
def _delete_channel_message(
|
|
364
|
+
self,
|
|
365
|
+
definition: Channel[object] | ChannelMap[object],
|
|
366
|
+
instance: str | None,
|
|
367
|
+
message_id: str,
|
|
368
|
+
) -> None:
|
|
369
|
+
if self._method is not InvocationMethod.RPC:
|
|
370
|
+
raise ValueError("Channel message deletion requires an RPC Context")
|
|
371
|
+
self._require_registered(definition)
|
|
372
|
+
name = self._physical_name(definition, instance)
|
|
373
|
+
self.channel_deletions.append(
|
|
374
|
+
pb.ChannelMessageDeletion(
|
|
375
|
+
channel_name=name,
|
|
376
|
+
message_id=require_name(message_id),
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
current = self._channel_infos.get(name)
|
|
380
|
+
if current is not None and current.size > 0:
|
|
381
|
+
self._channel_infos[name] = pb.ChannelInfo(size=current.size - 1)
|
|
382
|
+
|
|
352
383
|
def _attribute_map_keys(
|
|
353
384
|
self,
|
|
354
385
|
definition: AttributeMap[object],
|
|
355
386
|
) -> tuple[str, ...]:
|
|
356
387
|
self._require_registered(definition)
|
|
388
|
+
self._require_attribute_map_all_loaded(definition)
|
|
357
389
|
prefix = f"{definition.name}/"
|
|
358
390
|
physical_keys = {key for key in self._attributes if key.startswith(prefix)}
|
|
359
391
|
for key, write in self.attribute_writes.items():
|
|
@@ -363,7 +395,12 @@ class InvocationContext:
|
|
|
363
395
|
physical_keys.discard(key)
|
|
364
396
|
else:
|
|
365
397
|
physical_keys.add(key)
|
|
366
|
-
return tuple(
|
|
398
|
+
return tuple(
|
|
399
|
+
sorted(
|
|
400
|
+
require_map_instance(unquote(key[len(prefix) :]))
|
|
401
|
+
for key in physical_keys
|
|
402
|
+
)
|
|
403
|
+
)
|
|
367
404
|
|
|
368
405
|
def _channel_map_keys(
|
|
369
406
|
self,
|
|
@@ -375,7 +412,7 @@ class InvocationContext:
|
|
|
375
412
|
prefix = f"{definition.name}/"
|
|
376
413
|
return tuple(
|
|
377
414
|
sorted(
|
|
378
|
-
unquote(key[len(prefix) :])
|
|
415
|
+
require_map_instance(unquote(key[len(prefix) :]))
|
|
379
416
|
for key, info in self._channel_infos.items()
|
|
380
417
|
if key.startswith(prefix) and info.size > 0
|
|
381
418
|
)
|
|
@@ -390,6 +427,39 @@ class InvocationContext:
|
|
|
390
427
|
info = self._channel_infos.get(self._physical_name(definition, instance))
|
|
391
428
|
return info.size if info is not None else 0
|
|
392
429
|
|
|
430
|
+
def _pending_channel_messages(
|
|
431
|
+
self,
|
|
432
|
+
definition: Channel[ValueT] | ChannelMap[ValueT],
|
|
433
|
+
instance: str | None,
|
|
434
|
+
) -> tuple[ChannelMessage[ValueT], ...]:
|
|
435
|
+
if self._method is not InvocationMethod.RPC:
|
|
436
|
+
raise ValueError("pending Channel messages require an RPC Context")
|
|
437
|
+
self._require_registered(definition)
|
|
438
|
+
if isinstance(definition, ChannelMap):
|
|
439
|
+
channel_name = self._physical_name(definition, instance)
|
|
440
|
+
is_loaded = (
|
|
441
|
+
f"{definition.name}/" in self._loaded_channel_map_instances
|
|
442
|
+
or channel_name in self._loaded_channel_map_instances
|
|
443
|
+
)
|
|
444
|
+
else:
|
|
445
|
+
channel_name = self._physical_name(definition, instance)
|
|
446
|
+
is_loaded = definition.name in self._loaded_channel_names
|
|
447
|
+
if not is_loaded:
|
|
448
|
+
raise StateNotLoadedError(
|
|
449
|
+
f"Channel messages were not loaded for RPC: {definition.name}"
|
|
450
|
+
)
|
|
451
|
+
values = self._loaded_channel_messages.get(channel_name)
|
|
452
|
+
if values is None:
|
|
453
|
+
return ()
|
|
454
|
+
codec = self._flow_codec(definition.value_type)
|
|
455
|
+
return tuple(
|
|
456
|
+
ChannelMessage(
|
|
457
|
+
message_id=message.message_id,
|
|
458
|
+
value=cast(ValueT, self._values.decode(message.value, codec)),
|
|
459
|
+
)
|
|
460
|
+
for message in values.messages
|
|
461
|
+
)
|
|
462
|
+
|
|
393
463
|
def _channel_results(
|
|
394
464
|
self,
|
|
395
465
|
definition: Channel[ValueT] | ChannelMap[ValueT],
|
|
@@ -411,6 +481,36 @@ class InvocationContext:
|
|
|
411
481
|
def _flow_codec(self, value_type: object) -> Codec[Any]:
|
|
412
482
|
return self._values.codec(value_type)
|
|
413
483
|
|
|
484
|
+
def _require_attribute_map_instance_loaded(
|
|
485
|
+
self,
|
|
486
|
+
definition: Attribute[Any] | AttributeMap[Any],
|
|
487
|
+
instance: str | None,
|
|
488
|
+
) -> None:
|
|
489
|
+
if self._method is not InvocationMethod.RPC or not isinstance(
|
|
490
|
+
definition, AttributeMap
|
|
491
|
+
):
|
|
492
|
+
return
|
|
493
|
+
physical_name = self._physical_name(definition, instance)
|
|
494
|
+
if (
|
|
495
|
+
f"{definition.name}/" not in self._loaded_attribute_map_instances
|
|
496
|
+
and physical_name not in self._loaded_attribute_map_instances
|
|
497
|
+
):
|
|
498
|
+
raise StateNotLoadedError(
|
|
499
|
+
f"AttributeMap instance was not loaded for RPC: {physical_name}"
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def _require_attribute_map_all_loaded(
|
|
503
|
+
self,
|
|
504
|
+
definition: AttributeMap[object],
|
|
505
|
+
) -> None:
|
|
506
|
+
if (
|
|
507
|
+
self._method is InvocationMethod.RPC
|
|
508
|
+
and f"{definition.name}/" not in self._loaded_attribute_map_instances
|
|
509
|
+
):
|
|
510
|
+
raise StateNotLoadedError(
|
|
511
|
+
f"all AttributeMap instances were not loaded for RPC: {definition.name}"
|
|
512
|
+
)
|
|
513
|
+
|
|
414
514
|
def _require_registered(self, definition: _Definition) -> None:
|
|
415
515
|
registered = self._flow.persistence.get(definition.name)
|
|
416
516
|
if registered is not definition:
|
|
@@ -13,6 +13,20 @@ def require_name(name: str) -> str:
|
|
|
13
13
|
return name
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def require_persistence_definition_name(name: str) -> str:
|
|
17
|
+
require_name(name)
|
|
18
|
+
if "/" in name:
|
|
19
|
+
raise ValueError("persistence definition names must not contain '/'")
|
|
20
|
+
return name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_map_instance(instance: str) -> str:
|
|
24
|
+
require_name(instance)
|
|
25
|
+
if "/" in instance:
|
|
26
|
+
raise ValueError("map instances must not contain '/'")
|
|
27
|
+
return instance
|
|
28
|
+
|
|
29
|
+
|
|
16
30
|
def validate_condition_id(condition_id: str | None) -> None:
|
|
17
31
|
if condition_id is not None and not condition_id:
|
|
18
32
|
raise ValueError("condition ID must not be empty")
|
|
@@ -131,10 +131,24 @@ class ValueHydrator:
|
|
|
131
131
|
result = pb.InvokeWorkerRPCRequest()
|
|
132
132
|
result.CopyFrom(request)
|
|
133
133
|
values = [request.input, *(entry.value for entry in request.attributes)]
|
|
134
|
+
channel_messages = [
|
|
135
|
+
message
|
|
136
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
137
|
+
for message in channel_values.messages
|
|
138
|
+
]
|
|
139
|
+
values.extend(message.value for message in channel_messages)
|
|
134
140
|
hydrated = self.hydrate_all(values)
|
|
135
141
|
result.input.CopyFrom(hydrated[0])
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
hydrated_entries = iter(hydrated[1:])
|
|
143
|
+
for entry in result.attributes:
|
|
144
|
+
entry.value.CopyFrom(next(hydrated_entries))
|
|
145
|
+
result_messages = [
|
|
146
|
+
message
|
|
147
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
148
|
+
for message in channel_values.messages
|
|
149
|
+
]
|
|
150
|
+
for message in result_messages:
|
|
151
|
+
message.value.CopyFrom(next(hydrated_entries))
|
|
138
152
|
return result
|
|
139
153
|
|
|
140
154
|
def step_outputs(
|
|
@@ -219,6 +219,10 @@ class WorkerDispatcher:
|
|
|
219
219
|
self._values,
|
|
220
220
|
request.attributes,
|
|
221
221
|
channel_infos=dict(request.channel_infos),
|
|
222
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
223
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
224
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
225
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
222
226
|
is_active=is_active,
|
|
223
227
|
)
|
|
224
228
|
arguments: list[object] = [context]
|
|
@@ -233,6 +237,7 @@ class WorkerDispatcher:
|
|
|
233
237
|
response = pb.InvokeWorkerRPCResponse(
|
|
234
238
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
235
239
|
record_events=context.events,
|
|
240
|
+
delete_from_channel=context.channel_deletions,
|
|
236
241
|
publish_to_channel=context.publications,
|
|
237
242
|
)
|
|
238
243
|
if isinstance(returned, RPCResult):
|