dex-python-sdk 0.2.8__tar.gz → 0.2.9__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.9}/PKG-INFO +14 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/README.md +13 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/__init__.py +4 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_async_worker_dispatcher.py +1 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_grpc_errors.py +6 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_invocation_context.py +21 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_utils.py +7 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_worker_dispatcher.py +1 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/async_client.py +144 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/attribute.py +5 -5
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/channel.py +41 -5
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/client.py +142 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/context.py +8 -1
- dex_python_sdk-0.2.9/dex/dexpb/dex_pb2.py +469 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/dexpb/dex_pb2.pyi +78 -16
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/dexpb/dex_pb2_grpc.py +95 -9
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/flow.py +5 -1
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/runtime_errors.py +8 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/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.9}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/condition.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/flow_options.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/step.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/stream.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/wait.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/worker.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.8 → dex_python_sdk-0.2.9}/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.9
|
|
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,19 @@ 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
|
+
|
|
20
33
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
21
34
|
|
|
22
35
|
## New user contracts
|
|
@@ -1,6 +1,19 @@
|
|
|
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
|
+
|
|
4
17
|
Python SDK for [Dex workflow engine](https://github.com/superdurable/dex)
|
|
5
18
|
|
|
6
19
|
## New user contracts
|
|
@@ -19,7 +19,7 @@ from dex.attribute import (
|
|
|
19
19
|
IndexType,
|
|
20
20
|
)
|
|
21
21
|
from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
|
|
22
|
-
from dex.channel import Channel, ChannelMap
|
|
22
|
+
from dex.channel import Channel, ChannelMap, ChannelMessage
|
|
23
23
|
from dex.client import Client
|
|
24
24
|
from dex.client_options import ClientOptions
|
|
25
25
|
from dex.codec import (
|
|
@@ -68,6 +68,7 @@ from dex.runtime_errors import (
|
|
|
68
68
|
FlowNotFoundError,
|
|
69
69
|
InvalidStepResultError,
|
|
70
70
|
LongPollTimeoutError,
|
|
71
|
+
ChannelMessageNotFoundError,
|
|
71
72
|
RpcLockConflictError,
|
|
72
73
|
ValueMappingError,
|
|
73
74
|
WorkerInvocationError,
|
|
@@ -124,6 +125,8 @@ __all__ = [
|
|
|
124
125
|
"BufferedTextStream",
|
|
125
126
|
"Channel",
|
|
126
127
|
"ChannelMap",
|
|
128
|
+
"ChannelMessage",
|
|
129
|
+
"ChannelMessageNotFoundError",
|
|
127
130
|
"Client",
|
|
128
131
|
"ClientOptions",
|
|
129
132
|
"Codec",
|
|
@@ -306,6 +306,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
306
306
|
response = pb.InvokeWorkerRPCResponse(
|
|
307
307
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
308
308
|
record_events=context.events,
|
|
309
|
+
delete_from_channel=context.channel_deletions,
|
|
309
310
|
publish_to_channel=context.publications,
|
|
310
311
|
)
|
|
311
312
|
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)
|
|
@@ -82,6 +82,7 @@ class InvocationContext:
|
|
|
82
82
|
self.attribute_writes: dict[str, pb.AttributeWrite] = {}
|
|
83
83
|
self.local_writes: dict[str, pb.KV] = {}
|
|
84
84
|
self.events: list[pb.KV] = []
|
|
85
|
+
self.channel_deletions: list[pb.ChannelMessageDeletion] = []
|
|
85
86
|
self.publications: list[pb.ChannelMessage] = []
|
|
86
87
|
self._event_names: set[str] = set()
|
|
87
88
|
self._step_output_finalizers: list[_StepOutputFinalizer] = []
|
|
@@ -349,6 +350,26 @@ class InvocationContext:
|
|
|
349
350
|
size=(current.size if current is not None else 0) + 1
|
|
350
351
|
)
|
|
351
352
|
|
|
353
|
+
def _delete_channel_message(
|
|
354
|
+
self,
|
|
355
|
+
definition: Channel[object] | ChannelMap[object],
|
|
356
|
+
instance: str | None,
|
|
357
|
+
message_id: str,
|
|
358
|
+
) -> None:
|
|
359
|
+
if self._method is not InvocationMethod.RPC:
|
|
360
|
+
raise ValueError("Channel message deletion requires an RPC Context")
|
|
361
|
+
self._require_registered(definition)
|
|
362
|
+
name = self._physical_name(definition, instance)
|
|
363
|
+
self.channel_deletions.append(
|
|
364
|
+
pb.ChannelMessageDeletion(
|
|
365
|
+
channel_name=name,
|
|
366
|
+
message_id=require_name(message_id),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
current = self._channel_infos.get(name)
|
|
370
|
+
if current is not None and current.size > 0:
|
|
371
|
+
self._channel_infos[name] = pb.ChannelInfo(size=current.size - 1)
|
|
372
|
+
|
|
352
373
|
def _attribute_map_keys(
|
|
353
374
|
self,
|
|
354
375
|
definition: AttributeMap[object],
|
|
@@ -13,6 +13,13 @@ 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
|
+
|
|
16
23
|
def validate_condition_id(condition_id: str | None) -> None:
|
|
17
24
|
if condition_id is not None and not condition_id:
|
|
18
25
|
raise ValueError("condition ID must not be empty")
|
|
@@ -233,6 +233,7 @@ class WorkerDispatcher:
|
|
|
233
233
|
response = pb.InvokeWorkerRPCResponse(
|
|
234
234
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
235
235
|
record_events=context.events,
|
|
236
|
+
delete_from_channel=context.channel_deletions,
|
|
236
237
|
publish_to_channel=context.publications,
|
|
237
238
|
)
|
|
238
239
|
if isinstance(returned, RPCResult):
|
|
@@ -24,7 +24,7 @@ from dex._value_mapper import ValueMapper
|
|
|
24
24
|
from dex._worker_dispatcher import WorkerDispatcher
|
|
25
25
|
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
26
26
|
from dex.blob_cache import BlobCache
|
|
27
|
-
from dex.channel import Channel, ChannelMap
|
|
27
|
+
from dex.channel import Channel, ChannelMap, ChannelMessage
|
|
28
28
|
from dex.client_options import ClientOptions
|
|
29
29
|
from dex.context import Context
|
|
30
30
|
from dex.dexpb import dex_pb2 as pb
|
|
@@ -273,6 +273,7 @@ class AsyncClient:
|
|
|
273
273
|
timeout_seconds=timeout,
|
|
274
274
|
lock_attribute_keys=rpc.locks,
|
|
275
275
|
request_id=str(uuid4()),
|
|
276
|
+
is_transactional=rpc.options.is_transactional,
|
|
276
277
|
),
|
|
277
278
|
"invoke_rpc",
|
|
278
279
|
flow_id,
|
|
@@ -544,6 +545,148 @@ class AsyncClient:
|
|
|
544
545
|
"none",
|
|
545
546
|
)
|
|
546
547
|
|
|
548
|
+
@overload
|
|
549
|
+
async def get_channel_messages(
|
|
550
|
+
self,
|
|
551
|
+
flow_id: str,
|
|
552
|
+
channel: Channel[ValueT],
|
|
553
|
+
/,
|
|
554
|
+
*,
|
|
555
|
+
run_id: str = "",
|
|
556
|
+
) -> tuple[ChannelMessage[ValueT], ...]: ...
|
|
557
|
+
|
|
558
|
+
@overload
|
|
559
|
+
async def get_channel_messages(
|
|
560
|
+
self,
|
|
561
|
+
flow_id: str,
|
|
562
|
+
channel: ChannelMap[ValueT],
|
|
563
|
+
instance: str,
|
|
564
|
+
/,
|
|
565
|
+
*,
|
|
566
|
+
run_id: str = "",
|
|
567
|
+
) -> tuple[ChannelMessage[ValueT], ...]: ...
|
|
568
|
+
|
|
569
|
+
async def get_channel_messages(
|
|
570
|
+
self,
|
|
571
|
+
flow_id: str,
|
|
572
|
+
channel: Channel[Any] | ChannelMap[Any],
|
|
573
|
+
instance: str | None = None,
|
|
574
|
+
/,
|
|
575
|
+
*,
|
|
576
|
+
run_id: str = "",
|
|
577
|
+
) -> tuple[ChannelMessage[Any], ...]:
|
|
578
|
+
"""Return every pending message for a Channel in FIFO order.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
flow_id: The non-empty target Flow ID.
|
|
582
|
+
channel: A typed singleton Channel or ChannelMap definition.
|
|
583
|
+
instance: Required ChannelMap instance; omit for a singleton Channel.
|
|
584
|
+
run_id: Optional exact run; ``""`` targets the current run.
|
|
585
|
+
|
|
586
|
+
Returns:
|
|
587
|
+
Immutable typed message envelopes with server-assigned IDs.
|
|
588
|
+
|
|
589
|
+
Raises:
|
|
590
|
+
ValueMappingError: If a pending value cannot be decoded.
|
|
591
|
+
FlowNotFoundError: If the selected Flow run does not exist.
|
|
592
|
+
DexServiceError: If FlowService cannot read the queue.
|
|
593
|
+
"""
|
|
594
|
+
name = self._definition_name(channel, instance)
|
|
595
|
+
response = cast(
|
|
596
|
+
pb.GetChannelMessagesResponse,
|
|
597
|
+
await self._call(
|
|
598
|
+
self._service.GetChannelMessages,
|
|
599
|
+
pb.GetChannelMessagesRequest(
|
|
600
|
+
flow_id=require_name(flow_id),
|
|
601
|
+
run_id=run_id,
|
|
602
|
+
channel_name=name,
|
|
603
|
+
),
|
|
604
|
+
"get_channel_messages",
|
|
605
|
+
flow_id,
|
|
606
|
+
"existing",
|
|
607
|
+
),
|
|
608
|
+
)
|
|
609
|
+
codec = self._values.codec(channel.value_type)
|
|
610
|
+
messages: list[ChannelMessage[Any]] = []
|
|
611
|
+
for message in response.messages:
|
|
612
|
+
messages.append(
|
|
613
|
+
ChannelMessage(
|
|
614
|
+
message_id=message.message_id,
|
|
615
|
+
value=self._values.decode(
|
|
616
|
+
await self._hydrator.hydrate(message.value),
|
|
617
|
+
codec,
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
return tuple(messages)
|
|
622
|
+
|
|
623
|
+
@overload
|
|
624
|
+
async def delete_channel_message(
|
|
625
|
+
self,
|
|
626
|
+
flow_id: str,
|
|
627
|
+
channel: Channel[Any],
|
|
628
|
+
message_id: str,
|
|
629
|
+
/,
|
|
630
|
+
*,
|
|
631
|
+
run_id: str = "",
|
|
632
|
+
) -> None: ...
|
|
633
|
+
|
|
634
|
+
@overload
|
|
635
|
+
async def delete_channel_message(
|
|
636
|
+
self,
|
|
637
|
+
flow_id: str,
|
|
638
|
+
channel: ChannelMap[Any],
|
|
639
|
+
instance: str,
|
|
640
|
+
message_id: str,
|
|
641
|
+
/,
|
|
642
|
+
*,
|
|
643
|
+
run_id: str = "",
|
|
644
|
+
) -> None: ...
|
|
645
|
+
|
|
646
|
+
async def delete_channel_message(
|
|
647
|
+
self,
|
|
648
|
+
flow_id: str,
|
|
649
|
+
channel: Channel[Any] | ChannelMap[Any],
|
|
650
|
+
instance_or_message_id: str,
|
|
651
|
+
message_id: str | None = None,
|
|
652
|
+
/,
|
|
653
|
+
*,
|
|
654
|
+
run_id: str = "",
|
|
655
|
+
) -> None:
|
|
656
|
+
"""Delete one pending Channel message by its server-assigned ID.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
flow_id: The non-empty target Flow ID.
|
|
660
|
+
channel: A typed singleton Channel or ChannelMap definition.
|
|
661
|
+
instance_or_message_id: Singleton message ID or ChannelMap instance.
|
|
662
|
+
message_id: ChannelMap message ID; omit for a singleton Channel.
|
|
663
|
+
run_id: Optional exact run; ``""`` targets the active run.
|
|
664
|
+
|
|
665
|
+
Raises:
|
|
666
|
+
ChannelMessageNotFoundError: If the message is no longer pending.
|
|
667
|
+
FlowNotActiveError: If the selected Flow run is closed.
|
|
668
|
+
DexServiceError: If FlowService cannot delete the message.
|
|
669
|
+
"""
|
|
670
|
+
instance = instance_or_message_id if isinstance(channel, ChannelMap) else None
|
|
671
|
+
resolved_message_id = (
|
|
672
|
+
message_id if instance is not None else instance_or_message_id
|
|
673
|
+
)
|
|
674
|
+
if resolved_message_id is None:
|
|
675
|
+
raise ValueError("ChannelMap message ID is required")
|
|
676
|
+
await self._call(
|
|
677
|
+
self._service.DeleteChannelMessage,
|
|
678
|
+
pb.DeleteChannelMessageRequest(
|
|
679
|
+
flow_id=require_name(flow_id),
|
|
680
|
+
run_id=run_id,
|
|
681
|
+
channel_name=self._definition_name(channel, instance),
|
|
682
|
+
message_id=require_name(resolved_message_id),
|
|
683
|
+
request_id=str(uuid4()),
|
|
684
|
+
),
|
|
685
|
+
"delete_channel_message",
|
|
686
|
+
flow_id,
|
|
687
|
+
"active",
|
|
688
|
+
)
|
|
689
|
+
|
|
547
690
|
async def read_stream(
|
|
548
691
|
self,
|
|
549
692
|
flow_id: str,
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from typing import Any, Generic, TypeVar, cast
|
|
16
16
|
|
|
17
|
-
from dex._utils import require_name
|
|
17
|
+
from dex._utils import require_name, require_persistence_definition_name
|
|
18
18
|
from dex.context import Context
|
|
19
19
|
from dex.dexpb import dex_pb2 as pb
|
|
20
20
|
|
|
@@ -71,7 +71,7 @@ class Attribute(Generic[ValueT]):
|
|
|
71
71
|
projects SQL NULL, and failures do not roll back the Flow Attribute.
|
|
72
72
|
|
|
73
73
|
Attributes:
|
|
74
|
-
name: The non-empty logical Attribute name
|
|
74
|
+
name: The non-empty logical Attribute name without ``/``, unique within its Flow.
|
|
75
75
|
value_type: The Python type used to encode and decode values.
|
|
76
76
|
index: Optional search-index configuration; ``None`` disables indexing.
|
|
77
77
|
sync_to_attribute_store: Whether writes are projected to the selected
|
|
@@ -90,7 +90,7 @@ class Attribute(Generic[ValueT]):
|
|
|
90
90
|
sync_to_attribute_store: bool = False
|
|
91
91
|
|
|
92
92
|
def __post_init__(self) -> None:
|
|
93
|
-
|
|
93
|
+
require_persistence_definition_name(self.name)
|
|
94
94
|
|
|
95
95
|
def get(self, context: Context) -> ValueT:
|
|
96
96
|
"""Return the current value from a Step or RPC Context.
|
|
@@ -150,7 +150,7 @@ class AttributeMap(Generic[ValueT]):
|
|
|
150
150
|
do not roll back Flow Attributes.
|
|
151
151
|
|
|
152
152
|
Attributes:
|
|
153
|
-
name: The non-empty logical Attribute name
|
|
153
|
+
name: The non-empty logical Attribute name without ``/``, unique within its Flow.
|
|
154
154
|
value_type: The Python type used for every map instance.
|
|
155
155
|
index: Optional shared search-index configuration.
|
|
156
156
|
sync_to_attribute_store: Whether every instance is projected to the selected
|
|
@@ -169,7 +169,7 @@ class AttributeMap(Generic[ValueT]):
|
|
|
169
169
|
sync_to_attribute_store: bool = False
|
|
170
170
|
|
|
171
171
|
def __post_init__(self) -> None:
|
|
172
|
-
|
|
172
|
+
require_persistence_definition_name(self.name)
|
|
173
173
|
|
|
174
174
|
def get(self, context: Context, instance: str) -> ValueT:
|
|
175
175
|
"""Return one map instance from a Step or RPC Context.
|
|
@@ -13,13 +13,26 @@ from __future__ import annotations
|
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from typing import Generic, Sequence, TypeVar, cast
|
|
15
15
|
|
|
16
|
-
from dex._utils import
|
|
16
|
+
from dex._utils import require_persistence_definition_name
|
|
17
17
|
from dex.condition import ChannelCondition, Condition
|
|
18
18
|
from dex.context import Context
|
|
19
19
|
|
|
20
20
|
ValueT = TypeVar("ValueT")
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ChannelMessage(Generic[ValueT]):
|
|
25
|
+
"""Represent one pending Channel message and its server-assigned identity.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
message_id: UUIDv7 assigned by Dex when the message was published.
|
|
29
|
+
value: The decoded Channel value.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
message_id: str
|
|
33
|
+
value: ValueT
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
@dataclass(frozen=True)
|
|
24
37
|
class Channel(Generic[ValueT]):
|
|
25
38
|
"""Define a typed, durable singleton message stream owned by a Flow.
|
|
@@ -29,7 +42,7 @@ class Channel(Generic[ValueT]):
|
|
|
29
42
|
selected values through ``results``.
|
|
30
43
|
|
|
31
44
|
Attributes:
|
|
32
|
-
name: The non-empty Channel name
|
|
45
|
+
name: The non-empty Channel name without ``/``, unique within its Flow.
|
|
33
46
|
value_type: The Python type of every published value.
|
|
34
47
|
|
|
35
48
|
Examples:
|
|
@@ -42,7 +55,7 @@ class Channel(Generic[ValueT]):
|
|
|
42
55
|
value_type: type[ValueT]
|
|
43
56
|
|
|
44
57
|
def __post_init__(self) -> None:
|
|
45
|
-
|
|
58
|
+
require_persistence_definition_name(self.name)
|
|
46
59
|
|
|
47
60
|
def publish(self, context: Context, value: ValueT) -> None:
|
|
48
61
|
"""Stage one value to append with the current handler decision.
|
|
@@ -53,6 +66,19 @@ class Channel(Generic[ValueT]):
|
|
|
53
66
|
"""
|
|
54
67
|
context._publish_channel(self, None, value)
|
|
55
68
|
|
|
69
|
+
def delete(self, context: Context, message_id: str) -> None:
|
|
70
|
+
"""Stage deletion of one pending message from an RPC handler.
|
|
71
|
+
|
|
72
|
+
Use ``@rpc(is_transactional=True)`` when a missing message must fail the
|
|
73
|
+
entire RPC without committing its other writes. Step Contexts reject this
|
|
74
|
+
operation.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
context: The current RPC Context.
|
|
78
|
+
message_id: Non-empty ID returned by a Client pending-message read.
|
|
79
|
+
"""
|
|
80
|
+
context._delete_channel_message(self, None, message_id)
|
|
81
|
+
|
|
56
82
|
def size(self, context: Context) -> int:
|
|
57
83
|
"""Return the current number of queued values.
|
|
58
84
|
|
|
@@ -173,7 +199,7 @@ class ChannelMap(Generic[ValueT]):
|
|
|
173
199
|
Add the ChannelMap definition to the Flow's ``PersistenceSchema``.
|
|
174
200
|
|
|
175
201
|
Attributes:
|
|
176
|
-
name: The non-empty Channel name
|
|
202
|
+
name: The non-empty Channel name without ``/``, unique within its Flow.
|
|
177
203
|
value_type: The Python type of every published value.
|
|
178
204
|
|
|
179
205
|
Examples:
|
|
@@ -186,7 +212,7 @@ class ChannelMap(Generic[ValueT]):
|
|
|
186
212
|
value_type: type[ValueT]
|
|
187
213
|
|
|
188
214
|
def __post_init__(self) -> None:
|
|
189
|
-
|
|
215
|
+
require_persistence_definition_name(self.name)
|
|
190
216
|
|
|
191
217
|
def publish(self, context: Context, instance: str, value: ValueT) -> None:
|
|
192
218
|
"""Stage a value to append to one ChannelMap instance.
|
|
@@ -198,6 +224,16 @@ class ChannelMap(Generic[ValueT]):
|
|
|
198
224
|
"""
|
|
199
225
|
context._publish_channel(self, instance, value)
|
|
200
226
|
|
|
227
|
+
def delete(self, context: Context, instance: str, message_id: str) -> None:
|
|
228
|
+
"""Stage deletion of one pending message from a ChannelMap instance.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
context: The current RPC Context.
|
|
232
|
+
instance: The non-empty logical map key.
|
|
233
|
+
message_id: Non-empty ID returned by a Client pending-message read.
|
|
234
|
+
"""
|
|
235
|
+
context._delete_channel_message(self, instance, message_id)
|
|
236
|
+
|
|
201
237
|
def size(self, context: Context, instance: str) -> int:
|
|
202
238
|
"""Return the queued value count for one instance.
|
|
203
239
|
|
|
@@ -24,7 +24,7 @@ from dex._value_mapper import ValueMapper
|
|
|
24
24
|
from dex._worker_dispatcher import WorkerDispatcher
|
|
25
25
|
from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
|
|
26
26
|
from dex.blob_cache import BlobCache
|
|
27
|
-
from dex.channel import Channel, ChannelMap
|
|
27
|
+
from dex.channel import Channel, ChannelMap, ChannelMessage
|
|
28
28
|
from dex.client_options import ClientOptions
|
|
29
29
|
from dex.context import Context
|
|
30
30
|
from dex.dexpb import dex_pb2 as pb
|
|
@@ -273,6 +273,7 @@ class Client:
|
|
|
273
273
|
timeout_seconds=timeout,
|
|
274
274
|
lock_attribute_keys=rpc.locks,
|
|
275
275
|
request_id=str(uuid4()),
|
|
276
|
+
is_transactional=rpc.options.is_transactional,
|
|
276
277
|
),
|
|
277
278
|
"invoke_rpc",
|
|
278
279
|
flow_id,
|
|
@@ -545,6 +546,146 @@ class Client:
|
|
|
545
546
|
"none",
|
|
546
547
|
)
|
|
547
548
|
|
|
549
|
+
@overload
|
|
550
|
+
def get_channel_messages(
|
|
551
|
+
self,
|
|
552
|
+
flow_id: str,
|
|
553
|
+
channel: Channel[ValueT],
|
|
554
|
+
/,
|
|
555
|
+
*,
|
|
556
|
+
run_id: str = "",
|
|
557
|
+
) -> tuple[ChannelMessage[ValueT], ...]: ...
|
|
558
|
+
|
|
559
|
+
@overload
|
|
560
|
+
def get_channel_messages(
|
|
561
|
+
self,
|
|
562
|
+
flow_id: str,
|
|
563
|
+
channel: ChannelMap[ValueT],
|
|
564
|
+
instance: str,
|
|
565
|
+
/,
|
|
566
|
+
*,
|
|
567
|
+
run_id: str = "",
|
|
568
|
+
) -> tuple[ChannelMessage[ValueT], ...]: ...
|
|
569
|
+
|
|
570
|
+
def get_channel_messages(
|
|
571
|
+
self,
|
|
572
|
+
flow_id: str,
|
|
573
|
+
channel: Channel[Any] | ChannelMap[Any],
|
|
574
|
+
instance: str | None = None,
|
|
575
|
+
/,
|
|
576
|
+
*,
|
|
577
|
+
run_id: str = "",
|
|
578
|
+
) -> tuple[ChannelMessage[Any], ...]:
|
|
579
|
+
"""Return every pending message for a Channel in FIFO order.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
flow_id: The non-empty target Flow ID.
|
|
583
|
+
channel: A typed singleton Channel or ChannelMap definition.
|
|
584
|
+
instance: Required ChannelMap instance; omit for a singleton Channel.
|
|
585
|
+
run_id: Optional exact run; ``""`` targets the current run.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Immutable typed message envelopes with server-assigned IDs.
|
|
589
|
+
|
|
590
|
+
Raises:
|
|
591
|
+
ValueMappingError: If a pending value cannot be decoded.
|
|
592
|
+
FlowNotFoundError: If the selected Flow run does not exist.
|
|
593
|
+
DexServiceError: If FlowService cannot read the queue.
|
|
594
|
+
"""
|
|
595
|
+
name = self._definition_name(channel, instance)
|
|
596
|
+
response = cast(
|
|
597
|
+
pb.GetChannelMessagesResponse,
|
|
598
|
+
self._call(
|
|
599
|
+
self._service.GetChannelMessages,
|
|
600
|
+
pb.GetChannelMessagesRequest(
|
|
601
|
+
flow_id=require_name(flow_id),
|
|
602
|
+
run_id=run_id,
|
|
603
|
+
channel_name=name,
|
|
604
|
+
),
|
|
605
|
+
"get_channel_messages",
|
|
606
|
+
flow_id,
|
|
607
|
+
"existing",
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
codec = self._values.codec(channel.value_type)
|
|
611
|
+
return tuple(
|
|
612
|
+
ChannelMessage(
|
|
613
|
+
message_id=message.message_id,
|
|
614
|
+
value=self._values.decode(
|
|
615
|
+
self._hydrator.hydrate(message.value),
|
|
616
|
+
codec,
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
for message in response.messages
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
@overload
|
|
623
|
+
def delete_channel_message(
|
|
624
|
+
self,
|
|
625
|
+
flow_id: str,
|
|
626
|
+
channel: Channel[Any],
|
|
627
|
+
message_id: str,
|
|
628
|
+
/,
|
|
629
|
+
*,
|
|
630
|
+
run_id: str = "",
|
|
631
|
+
) -> None: ...
|
|
632
|
+
|
|
633
|
+
@overload
|
|
634
|
+
def delete_channel_message(
|
|
635
|
+
self,
|
|
636
|
+
flow_id: str,
|
|
637
|
+
channel: ChannelMap[Any],
|
|
638
|
+
instance: str,
|
|
639
|
+
message_id: str,
|
|
640
|
+
/,
|
|
641
|
+
*,
|
|
642
|
+
run_id: str = "",
|
|
643
|
+
) -> None: ...
|
|
644
|
+
|
|
645
|
+
def delete_channel_message(
|
|
646
|
+
self,
|
|
647
|
+
flow_id: str,
|
|
648
|
+
channel: Channel[Any] | ChannelMap[Any],
|
|
649
|
+
instance_or_message_id: str,
|
|
650
|
+
message_id: str | None = None,
|
|
651
|
+
/,
|
|
652
|
+
*,
|
|
653
|
+
run_id: str = "",
|
|
654
|
+
) -> None:
|
|
655
|
+
"""Delete one pending Channel message by its server-assigned ID.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
flow_id: The non-empty target Flow ID.
|
|
659
|
+
channel: A typed singleton Channel or ChannelMap definition.
|
|
660
|
+
instance_or_message_id: Singleton message ID or ChannelMap instance.
|
|
661
|
+
message_id: ChannelMap message ID; omit for a singleton Channel.
|
|
662
|
+
run_id: Optional exact run; ``""`` targets the active run.
|
|
663
|
+
|
|
664
|
+
Raises:
|
|
665
|
+
ChannelMessageNotFoundError: If the message is no longer pending.
|
|
666
|
+
FlowNotActiveError: If the selected Flow run is closed.
|
|
667
|
+
DexServiceError: If FlowService cannot delete the message.
|
|
668
|
+
"""
|
|
669
|
+
instance = instance_or_message_id if isinstance(channel, ChannelMap) else None
|
|
670
|
+
resolved_message_id = (
|
|
671
|
+
message_id if instance is not None else instance_or_message_id
|
|
672
|
+
)
|
|
673
|
+
if resolved_message_id is None:
|
|
674
|
+
raise ValueError("ChannelMap message ID is required")
|
|
675
|
+
self._call(
|
|
676
|
+
self._service.DeleteChannelMessage,
|
|
677
|
+
pb.DeleteChannelMessageRequest(
|
|
678
|
+
flow_id=require_name(flow_id),
|
|
679
|
+
run_id=run_id,
|
|
680
|
+
channel_name=self._definition_name(channel, instance),
|
|
681
|
+
message_id=require_name(resolved_message_id),
|
|
682
|
+
request_id=str(uuid4()),
|
|
683
|
+
),
|
|
684
|
+
"delete_channel_message",
|
|
685
|
+
flow_id,
|
|
686
|
+
"active",
|
|
687
|
+
)
|
|
688
|
+
|
|
548
689
|
def read_stream(
|
|
549
690
|
self,
|
|
550
691
|
flow_id: str,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
from typing import TYPE_CHECKING, Protocol, Sequence, TypeVar
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Protocol, Sequence, TypeVar
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from dex.attribute import Attribute, AttributeMap
|
|
@@ -210,6 +210,13 @@ class Context(Protocol):
|
|
|
210
210
|
value: ValueT,
|
|
211
211
|
) -> None: ...
|
|
212
212
|
|
|
213
|
+
def _delete_channel_message(
|
|
214
|
+
self,
|
|
215
|
+
definition: Channel[Any] | ChannelMap[Any],
|
|
216
|
+
instance: str | None,
|
|
217
|
+
message_id: str,
|
|
218
|
+
) -> None: ...
|
|
219
|
+
|
|
213
220
|
def _channel_size(
|
|
214
221
|
self,
|
|
215
222
|
definition: Channel[object] | ChannelMap[object],
|