dex-python-sdk 0.4.0__tar.gz → 0.6.0__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.
Files changed (63) hide show
  1. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/PKG-INFO +17 -9
  2. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/README.md +16 -8
  3. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/__init__.py +2 -0
  4. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/async_client.py +69 -376
  5. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/client.py +69 -375
  6. dex_python_sdk-0.6.0/dex/dexpb/dex_pb2.py +501 -0
  7. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/dexpb/dex_pb2.pyi +22 -0
  8. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/dexpb/dex_pb2_grpc.py +43 -0
  9. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/stream.py +17 -1
  10. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/pyproject.toml +1 -1
  11. dex_python_sdk-0.4.0/dex/dexpb/dex_pb2.py +0 -497
  12. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/LEGACY_NOTICES.md +0 -0
  13. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/LICENSE +0 -0
  14. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_async_value_hydrator.py +0 -0
  15. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_async_worker_dispatcher.py +0 -0
  16. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_async_worker_service.py +0 -0
  17. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_grpc_errors.py +0 -0
  18. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_invocation_context.py +0 -0
  19. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_native.pyi +0 -0
  20. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_utils.py +0 -0
  21. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_value_hydrator.py +0 -0
  22. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_value_mapper.py +0 -0
  23. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_worker_dispatcher.py +0 -0
  24. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/_worker_service.py +0 -0
  25. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/async_worker.py +0 -0
  26. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/attribute.py +0 -0
  27. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/attribute_match.py +0 -0
  28. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/blob_cache.py +0 -0
  29. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/channel.py +0 -0
  30. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/client_options.py +0 -0
  31. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/codec.py +0 -0
  32. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/condition.py +0 -0
  33. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/context.py +0 -0
  34. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/dexpb/__init__.py +0 -0
  35. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/flow.py +0 -0
  36. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/flow_config.py +0 -0
  37. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/flow_info.py +0 -0
  38. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/flow_options.py +0 -0
  39. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/flow_result.py +0 -0
  40. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/py.typed +0 -0
  41. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/runtime_errors.py +0 -0
  42. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/step.py +0 -0
  43. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/step_execution.py +0 -0
  44. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/subflow.py +0 -0
  45. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/timer.py +0 -0
  46. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/wait.py +0 -0
  47. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/worker.py +0 -0
  48. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/dex/worker_options.py +0 -0
  49. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/Cargo.lock +0 -0
  50. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/Cargo.toml +0 -0
  51. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
  52. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  53. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
  54. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  55. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
  56. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  57. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
  58. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  59. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  60. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
  61. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
  62. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  63. {dex_python_sdk-0.4.0 → dex_python_sdk-0.6.0}/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.4.0
3
+ Version: 0.6.0
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,13 +17,12 @@ Project-URL: Repository, https://github.com/superdurable/dex
17
17
 
18
18
  # Dex SDK for Python
19
19
 
20
- ## Pending Channel messages
20
+ ## Flow state I/O
21
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.
22
+ Applications read and write Flow state through typed RPCs. Client and AsyncClient
23
+ do not expose direct Attribute reads or writes, Channel publication, or
24
+ pending-message mutation. This keeps each external state transition behind a
25
+ Flow-owned method.
27
26
 
28
27
  RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
29
28
  `@rpc(is_transactional=True)` when a missing message must abort its other writes.
@@ -279,6 +278,15 @@ message = client.read_stream(flow_id, progress)
279
278
  print(message.source)
280
279
  ```
281
280
 
281
+ `Client.read_stream` and `AsyncClient.read_stream` move forward one message at
282
+ a time and can long-poll. Their `list_stream_messages` methods return one
283
+ non-blocking newest-first retained page. Pass the registered `Stream`, a
284
+ positive page size, and an empty token for the first page. Pass
285
+ `StreamMessagesPage.next_page_token` unchanged to read older messages until it
286
+ is empty. The server caps page size at 1000 by default. A trimmed anchor returns
287
+ an empty page, and newer concurrent writes do not enter an existing older-page
288
+ chain.
289
+
282
290
  ### Canceling Step executions
283
291
 
284
292
  A successful Step can cancel queued or active executions while continuing with
@@ -392,14 +400,14 @@ SubFlow.
392
400
  ### Errors
393
401
 
394
402
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
395
- (`get_attribute`, `describe_flow`, `wait_for_flow`, and `time_travel`) raise
403
+ (`describe_flow`, `wait_for_flow`, and `time_travel`) raise
396
404
  `FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
397
405
  waits, config updates, and continue-as-new triggers raise
398
406
  `FlowNotActiveError` when no running Flow can accept the operation.
399
407
 
400
408
  ```python
401
409
  try:
402
- client.publish(flow_id, orders.approved, order_id)
410
+ client.invoke_rpc(orders.update_order, order_id, update)
403
411
  except dex.FlowNotActiveError:
404
412
  # The Flow is missing or already closed.
405
413
  pass
@@ -1,13 +1,12 @@
1
1
 
2
2
  # Dex SDK for Python
3
3
 
4
- ## Pending Channel messages
4
+ ## Flow state I/O
5
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.
6
+ Applications read and write Flow state through typed RPCs. Client and AsyncClient
7
+ do not expose direct Attribute reads or writes, Channel publication, or
8
+ pending-message mutation. This keeps each external state transition behind a
9
+ Flow-owned method.
11
10
 
12
11
  RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
13
12
  `@rpc(is_transactional=True)` when a missing message must abort its other writes.
@@ -263,6 +262,15 @@ message = client.read_stream(flow_id, progress)
263
262
  print(message.source)
264
263
  ```
265
264
 
265
+ `Client.read_stream` and `AsyncClient.read_stream` move forward one message at
266
+ a time and can long-poll. Their `list_stream_messages` methods return one
267
+ non-blocking newest-first retained page. Pass the registered `Stream`, a
268
+ positive page size, and an empty token for the first page. Pass
269
+ `StreamMessagesPage.next_page_token` unchanged to read older messages until it
270
+ is empty. The server caps page size at 1000 by default. A trimmed anchor returns
271
+ an empty page, and newer concurrent writes do not enter an existing older-page
272
+ chain.
273
+
266
274
  ### Canceling Step executions
267
275
 
268
276
  A successful Step can cancel queued or active executions while continuing with
@@ -376,14 +384,14 @@ SubFlow.
376
384
  ### Errors
377
385
 
378
386
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
379
- (`get_attribute`, `describe_flow`, `wait_for_flow`, and `time_travel`) raise
387
+ (`describe_flow`, `wait_for_flow`, and `time_travel`) raise
380
388
  `FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
381
389
  waits, config updates, and continue-as-new triggers raise
382
390
  `FlowNotActiveError` when no running Flow can accept the operation.
383
391
 
384
392
  ```python
385
393
  try:
386
- client.publish(flow_id, orders.approved, order_id)
394
+ client.invoke_rpc(orders.update_order, order_id, update)
387
395
  except dex.FlowNotActiveError:
388
396
  # The Flow is missing or already closed.
389
397
  pass
@@ -103,6 +103,7 @@ from dex.stream import (
103
103
  BufferedTextStream,
104
104
  Stream,
105
105
  StreamMessage,
106
+ StreamMessagesPage,
106
107
  )
107
108
  from dex.subflow import SubFlow
108
109
  from dex.timer import Timer
@@ -190,6 +191,7 @@ __all__ = [
190
191
  "StepOptions",
191
192
  "Stream",
192
193
  "StreamMessage",
194
+ "StreamMessagesPage",
193
195
  "StopFlowOptions",
194
196
  "StopType",
195
197
  "Timer",
@@ -25,7 +25,7 @@ from dex._worker_dispatcher import WorkerDispatcher
25
25
  from dex.attribute import Attribute, AttributeMap, _apply_attribute_store_sync
26
26
  from dex.attribute_match import AttributeMatch, _encode_attribute_match
27
27
  from dex.blob_cache import BlobCache
28
- from dex.channel import Channel, ChannelMap, ChannelMessage
28
+ from dex.channel import Channel, ChannelMap
29
29
  from dex.client_options import ClientOptions
30
30
  from dex.context import Context
31
31
  from dex.dexpb import dex_pb2 as pb
@@ -48,7 +48,7 @@ from dex.flow_result import FlowResult, flow_result_from_proto
48
48
  from dex.runtime_errors import FlowErrorType
49
49
  from dex.step import RetryPolicy, StepDurability
50
50
  from dex.step_execution import StepExecutionId, TimerId
51
- from dex.stream import Stream, StreamMessage
51
+ from dex.stream import Stream, StreamMessage, StreamMessagesPage
52
52
 
53
53
  InputT = TypeVar("InputT")
54
54
  OutputT = TypeVar("OutputT")
@@ -307,223 +307,6 @@ class AsyncClient:
307
307
  rpc.output_codec,
308
308
  )
309
309
 
310
- @overload
311
- async def get_attribute(
312
- self,
313
- flow_id: str,
314
- attribute: Attribute[ValueT],
315
- *,
316
- run_id: str = "",
317
- ) -> ValueT: ...
318
-
319
- @overload
320
- async def get_attribute(
321
- self,
322
- flow_id: str,
323
- attribute: AttributeMap[ValueT],
324
- instance: str,
325
- *,
326
- run_id: str = "",
327
- ) -> ValueT: ...
328
-
329
- async def get_attribute(
330
- self,
331
- flow_id: str,
332
- attribute: Attribute[Any] | AttributeMap[Any],
333
- instance: str | None = None,
334
- *,
335
- run_id: str = "",
336
- ) -> Any:
337
- """Await one decoded Attribute or AttributeMap instance.
338
-
339
- Args:
340
- flow_id: The non-empty target Flow ID.
341
- attribute: A typed singleton Attribute or AttributeMap definition.
342
- instance: The map instance. Omit it for a singleton Attribute. Slash is prohibited because it is a reserved character.
343
- run_id: Optional exact run; ``""`` targets the current run.
344
-
345
- Returns:
346
- The decoded value, or ``None`` when unset.
347
-
348
- Raises:
349
- TypeError: If singleton/map arguments do not match the definition.
350
- ValueMappingError: If the stored value cannot be decoded.
351
- FlowNotFoundError: If ``flow_id`` does not exist.
352
- DexServiceError: If FlowService cannot perform the request.
353
- """
354
- key = self._definition_name(attribute, instance)
355
- response = cast(
356
- pb.GetAttributesResponse,
357
- await self._call(
358
- self._service.GetAttributes,
359
- pb.GetAttributesRequest(
360
- flow_id=require_name(flow_id),
361
- run_id=run_id,
362
- keys=[key],
363
- ),
364
- "get_attribute",
365
- flow_id,
366
- "existing",
367
- ),
368
- )
369
- if not response.attributes:
370
- return None
371
- value = await self._hydrator.hydrate(response.attributes[0].value)
372
- return self._values.decode(value, self._values.codec(attribute.value_type))
373
-
374
- @overload
375
- async def set_attribute(
376
- self,
377
- flow_id: str,
378
- attribute: Attribute[ValueT],
379
- value: ValueT,
380
- /,
381
- *,
382
- run_id: str = "",
383
- ) -> None: ...
384
-
385
- @overload
386
- async def set_attribute(
387
- self,
388
- flow_id: str,
389
- attribute: AttributeMap[ValueT],
390
- instance: str,
391
- value: ValueT,
392
- /,
393
- *,
394
- run_id: str = "",
395
- ) -> None: ...
396
-
397
- async def set_attribute(
398
- self,
399
- flow_id: str,
400
- attribute: Attribute[Any] | AttributeMap[Any],
401
- /,
402
- *args: object,
403
- run_id: str = "",
404
- ) -> None:
405
- """Write one Attribute or AttributeMap instance on an active Flow.
406
-
407
- Singleton form is ``set_attribute(flow_id, attribute, value)``; map form is
408
- ``set_attribute(flow_id, attribute_map, instance, value)``.
409
-
410
- Args:
411
- flow_id: The non-empty target Flow ID.
412
- attribute: A typed singleton Attribute or AttributeMap definition.
413
- *args: The value, or a map instance followed by its value.
414
- run_id: Optional exact run; ``""`` targets the active run.
415
-
416
- Raises:
417
- TypeError: If arguments or value type are invalid.
418
- ValueMappingError: If the value cannot be encoded.
419
- FlowNotActiveError: If the selected Flow run is closed.
420
- DexServiceError: If FlowService cannot apply the write.
421
- """
422
- instance, value = self._definition_value(attribute, args)
423
- write = pb.AttributeWrite(
424
- key=self._definition_name(attribute, instance),
425
- value=self._values.encode(
426
- value,
427
- self._values.codec(attribute.value_type),
428
- ),
429
- )
430
- index = self._values.index_config(
431
- attribute.index,
432
- isinstance(attribute, AttributeMap),
433
- )
434
- if index is not None:
435
- write.index_config.CopyFrom(index)
436
- _apply_attribute_store_sync(write, attribute)
437
- await self._call(
438
- self._service.SetAttributes,
439
- pb.SetAttributesRequest(
440
- flow_id=require_name(flow_id),
441
- run_id=run_id,
442
- attributes=[write],
443
- request_id=str(uuid4()),
444
- ),
445
- "set_attribute",
446
- flow_id,
447
- "active",
448
- )
449
-
450
- @overload
451
- async def publish(
452
- self,
453
- flow_id: str,
454
- channel: Channel[ValueT],
455
- /,
456
- *values: ValueT,
457
- run_id: str = "",
458
- ) -> None: ...
459
-
460
- @overload
461
- async def publish(
462
- self,
463
- flow_id: str,
464
- channel: ChannelMap[ValueT],
465
- instance: str,
466
- /,
467
- *values: ValueT,
468
- run_id: str = "",
469
- ) -> None: ...
470
-
471
- async def publish(
472
- self,
473
- flow_id: str,
474
- channel: Channel[Any] | ChannelMap[Any],
475
- /,
476
- *args: object,
477
- run_id: str = "",
478
- ) -> None:
479
- """Append one or more typed values to a Channel.
480
-
481
- Singleton form supplies values directly; ChannelMap form supplies an instance
482
- followed by values. Dex preserves argument order within the request.
483
-
484
- Args:
485
- flow_id: The non-empty target Flow ID.
486
- channel: A typed singleton Channel or ChannelMap definition.
487
- *args: Values, or a map instance followed by one or more values.
488
- run_id: Optional exact run; ``""`` targets the active run.
489
-
490
- Raises:
491
- ValueError: If no value is passed or a name is empty.
492
- TypeError: If map arguments or value types are invalid.
493
- ValueMappingError: If a value cannot be encoded.
494
- FlowNotActiveError: If the selected Flow run is closed.
495
- DexServiceError: If FlowService cannot publish the batch.
496
- """
497
- if isinstance(channel, ChannelMap):
498
- if len(args) < 2 or not isinstance(args[0], str):
499
- raise TypeError("ChannelMap publish requires instance and values")
500
- instance = args[0]
501
- values = args[1:]
502
- else:
503
- instance = None
504
- values = args
505
- if not values:
506
- raise ValueError("publish requires at least one value")
507
- name = self._definition_name(channel, instance)
508
- codec = self._values.codec(channel.value_type)
509
- await self._call(
510
- self._service.PublishToChannel,
511
- pb.PublishToChannelRequest(
512
- flow_id=require_name(flow_id),
513
- run_id=run_id,
514
- messages=[
515
- pb.ChannelMessage(
516
- channel_name=name,
517
- value=self._values.encode(value, codec),
518
- )
519
- for value in values
520
- ],
521
- ),
522
- "publish",
523
- flow_id,
524
- "active",
525
- )
526
-
527
310
  async def write_stream(
528
311
  self,
529
312
  flow_id: str,
@@ -565,148 +348,6 @@ class AsyncClient:
565
348
  "none",
566
349
  )
567
350
 
568
- @overload
569
- async def get_channel_messages(
570
- self,
571
- flow_id: str,
572
- channel: Channel[ValueT],
573
- /,
574
- *,
575
- run_id: str = "",
576
- ) -> tuple[ChannelMessage[ValueT], ...]: ...
577
-
578
- @overload
579
- async def get_channel_messages(
580
- self,
581
- flow_id: str,
582
- channel: ChannelMap[ValueT],
583
- instance: str,
584
- /,
585
- *,
586
- run_id: str = "",
587
- ) -> tuple[ChannelMessage[ValueT], ...]: ...
588
-
589
- async def get_channel_messages(
590
- self,
591
- flow_id: str,
592
- channel: Channel[Any] | ChannelMap[Any],
593
- instance: str | None = None,
594
- /,
595
- *,
596
- run_id: str = "",
597
- ) -> tuple[ChannelMessage[Any], ...]:
598
- """Return every pending message for a Channel in FIFO order.
599
-
600
- Args:
601
- flow_id: The non-empty target Flow ID.
602
- channel: A typed singleton Channel or ChannelMap definition.
603
- instance: Required ChannelMap instance; omit for a singleton Channel.
604
- run_id: Optional exact run; ``""`` targets the current run.
605
-
606
- Returns:
607
- Immutable typed message envelopes with server-assigned IDs.
608
-
609
- Raises:
610
- ValueMappingError: If a pending value cannot be decoded.
611
- FlowNotFoundError: If the selected Flow run does not exist.
612
- DexServiceError: If FlowService cannot read the queue.
613
- """
614
- name = self._definition_name(channel, instance)
615
- response = cast(
616
- pb.GetChannelMessagesResponse,
617
- await self._call(
618
- self._service.GetChannelMessages,
619
- pb.GetChannelMessagesRequest(
620
- flow_id=require_name(flow_id),
621
- run_id=run_id,
622
- channel_name=name,
623
- ),
624
- "get_channel_messages",
625
- flow_id,
626
- "existing",
627
- ),
628
- )
629
- codec = self._values.codec(channel.value_type)
630
- messages: list[ChannelMessage[Any]] = []
631
- for message in response.messages:
632
- messages.append(
633
- ChannelMessage(
634
- message_id=message.message_id,
635
- value=self._values.decode(
636
- await self._hydrator.hydrate(message.value),
637
- codec,
638
- ),
639
- )
640
- )
641
- return tuple(messages)
642
-
643
- @overload
644
- async def delete_channel_message(
645
- self,
646
- flow_id: str,
647
- channel: Channel[Any],
648
- message_id: str,
649
- /,
650
- *,
651
- run_id: str = "",
652
- ) -> None: ...
653
-
654
- @overload
655
- async def delete_channel_message(
656
- self,
657
- flow_id: str,
658
- channel: ChannelMap[Any],
659
- instance: str,
660
- message_id: str,
661
- /,
662
- *,
663
- run_id: str = "",
664
- ) -> None: ...
665
-
666
- async def delete_channel_message(
667
- self,
668
- flow_id: str,
669
- channel: Channel[Any] | ChannelMap[Any],
670
- instance_or_message_id: str,
671
- message_id: str | None = None,
672
- /,
673
- *,
674
- run_id: str = "",
675
- ) -> None:
676
- """Delete one pending Channel message by its server-assigned ID.
677
-
678
- Args:
679
- flow_id: The non-empty target Flow ID.
680
- channel: A typed singleton Channel or ChannelMap definition.
681
- instance_or_message_id: Singleton message ID or ChannelMap instance.
682
- message_id: ChannelMap message ID; omit for a singleton Channel.
683
- run_id: Optional exact run; ``""`` targets the active run.
684
-
685
- Raises:
686
- ChannelMessageNotFoundError: If the message is no longer pending.
687
- FlowNotActiveError: If the selected Flow run is closed.
688
- DexServiceError: If FlowService cannot delete the message.
689
- """
690
- instance = instance_or_message_id if isinstance(channel, ChannelMap) else None
691
- resolved_message_id = (
692
- message_id if instance is not None else instance_or_message_id
693
- )
694
- if resolved_message_id is None:
695
- raise ValueError("ChannelMap message ID is required")
696
- await self._call(
697
- self._service.DeleteChannelMessage,
698
- pb.DeleteChannelMessageRequest(
699
- flow_id=require_name(flow_id),
700
- run_id=run_id,
701
- channel_name=self._definition_name(channel, instance),
702
- message_id=require_name(resolved_message_id),
703
- request_id=str(uuid4()),
704
- ),
705
- "delete_channel_message",
706
- flow_id,
707
- "active",
708
- )
709
-
710
351
  async def read_stream(
711
352
  self,
712
353
  flow_id: str,
@@ -767,6 +408,73 @@ class AsyncClient:
767
408
  response.message.source,
768
409
  )
769
410
 
411
+ async def list_stream_messages(
412
+ self,
413
+ flow_id: str,
414
+ stream: Stream[ValueT],
415
+ page_size: int,
416
+ before_page_token: str = "",
417
+ ) -> StreamMessagesPage[ValueT]:
418
+ """Await one newest-first page of retained Stream messages without waiting for writes.
419
+
420
+ An empty token starts at the retained tail. Pass the returned page token
421
+ unchanged to read older messages. Trimming may remove messages between pages.
422
+
423
+ Args:
424
+ flow_id: Logical Flow instance ID used as the Stream instance key.
425
+ stream: Exact Stream object registered in one Flow schema.
426
+ page_size: Positive maximum message count within the server-configured limit.
427
+ before_page_token: Opaque token from the preceding page, or ``""`` first.
428
+
429
+ Returns:
430
+ Decoded messages in newest-first order and the next-page token.
431
+
432
+ Raises:
433
+ ValueError: If ``page_size`` is not positive.
434
+ FlowDefinitionError: If the Stream is not registered.
435
+ ValueMappingError: If a retained message cannot be decoded.
436
+ DexServiceError: If FlowService cannot list the Stream.
437
+ """
438
+ if page_size < 1:
439
+ raise ValueError("Stream page size must be positive")
440
+ flow = self.registry._flow_for_stream(stream)
441
+ response = cast(
442
+ pb.ListStreamMessagesResponse,
443
+ await self._call(
444
+ self._service.ListStreamMessages,
445
+ pb.ListStreamMessagesRequest(
446
+ flow_id=require_name(flow_id),
447
+ flow_type=flow.name,
448
+ stream_name=stream.name,
449
+ page_size=page_size,
450
+ before_page_token=before_page_token,
451
+ ),
452
+ "list_stream_messages",
453
+ flow_id,
454
+ "none",
455
+ ),
456
+ )
457
+ messages: list[StreamMessage[ValueT]] = []
458
+ for message in response.messages:
459
+ if (
460
+ not message.HasField("value")
461
+ or not message.HasField("created_time")
462
+ or not message.resume_token
463
+ ):
464
+ raise ValueError("Dex returned an incomplete Stream message")
465
+ messages.append(
466
+ StreamMessage(
467
+ self._values.decode(
468
+ message.value,
469
+ self._values.codec(stream.value_type),
470
+ ),
471
+ message.resume_token,
472
+ message.created_time.ToDatetime(tzinfo=timezone.utc),
473
+ message.source,
474
+ )
475
+ )
476
+ return StreamMessagesPage(messages, response.next_page_token)
477
+
770
478
  async def wait_for_flow(
771
479
  self,
772
480
  flow_id: str,
@@ -1395,21 +1103,6 @@ class AsyncClient:
1395
1103
  raise ValueError("static definition cannot use an instance")
1396
1104
  return definition.name
1397
1105
 
1398
- @staticmethod
1399
- def _definition_value(
1400
- definition: Attribute[Any] | AttributeMap[Any],
1401
- args: tuple[object, ...],
1402
- ) -> tuple[str | None, object]:
1403
- if isinstance(definition, Attribute) and len(args) == 1:
1404
- return None, args[0]
1405
- if (
1406
- isinstance(definition, AttributeMap)
1407
- and len(args) == 2
1408
- and isinstance(args[0], str)
1409
- ):
1410
- return args[0], args[1]
1411
- raise TypeError("set_attribute received invalid arguments")
1412
-
1413
1106
  @staticmethod
1414
1107
  def _attribute_wait_arguments(
1415
1108
  definition: Attribute[Any] | AttributeMap[Any],