dex-python-sdk 0.1.2__tar.gz → 0.1.3__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 (58) hide show
  1. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/PKG-INFO +15 -1
  2. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/README.md +14 -0
  3. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/__init__.py +2 -2
  4. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_invocation_context.py +33 -0
  5. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_worker_dispatcher.py +14 -12
  6. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/async_client.py +96 -0
  7. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/async_worker.py +3 -3
  8. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/attribute.py +22 -0
  9. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/channel.py +22 -0
  10. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/client.py +93 -0
  11. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/context.py +10 -0
  12. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/step.py +1 -1
  13. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/pyproject.toml +1 -1
  14. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/LEGACY_NOTICES.md +0 -0
  15. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/LICENSE +0 -0
  16. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_async_value_hydrator.py +0 -0
  17. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_async_worker_dispatcher.py +0 -0
  18. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_async_worker_service.py +0 -0
  19. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_grpc_errors.py +0 -0
  20. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_native.pyi +0 -0
  21. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_utils.py +0 -0
  22. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_value_hydrator.py +0 -0
  23. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_value_mapper.py +0 -0
  24. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/_worker_service.py +0 -0
  25. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/blob_cache.py +0 -0
  26. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/client_options.py +0 -0
  27. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/codec.py +0 -0
  28. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/condition.py +0 -0
  29. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/dexpb/__init__.py +0 -0
  30. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/dexpb/dex_pb2.py +0 -0
  31. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/dexpb/dex_pb2.pyi +0 -0
  32. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/dexpb/dex_pb2_grpc.py +0 -0
  33. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/flow.py +0 -0
  34. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/flow_config.py +0 -0
  35. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/flow_info.py +0 -0
  36. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/flow_options.py +0 -0
  37. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/py.typed +0 -0
  38. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/runtime_errors.py +0 -0
  39. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/step_execution.py +0 -0
  40. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/timer.py +0 -0
  41. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/wait.py +0 -0
  42. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/worker.py +0 -0
  43. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/dex/worker_options.py +0 -0
  44. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/Cargo.lock +0 -0
  45. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/Cargo.toml +0 -0
  46. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
  47. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
  48. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
  49. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
  50. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
  51. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
  52. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
  53. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
  54. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
  55. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
  56. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
  57. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
  58. {dex_python_sdk-0.1.2 → dex_python_sdk-0.1.3}/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.1.2
3
+ Version: 0.1.3
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
@@ -135,6 +135,20 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
135
135
  codec before Client or Worker startup. `Client` methods use these typed objects
136
136
  instead of raw Flow, Step, or RPC strings.
137
137
 
138
+ ### Waiting and map inspection
139
+
140
+ `Wait.all_of` and `Wait.any_of` may use unnamed Conditions. Every Condition in
141
+ `Wait.any_combination_of` must have a non-empty user ID; the same Condition
142
+ instance may appear in multiple combinations.
143
+
144
+ Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
145
+ `wait_for_attribute_map_equal`. They target the current run and accept only
146
+ string, bool, int, or float wire values. JSON objects, bytes, and null fail
147
+ before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
148
+ buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
149
+ include buffered publishes, and omit empty instances. Keys are decoded and
150
+ sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
151
+
138
152
  ### Errors
139
153
 
140
154
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
@@ -119,6 +119,20 @@ two handler calls. Timer and channel conditions determine how long a Step waits.
119
119
  codec before Client or Worker startup. `Client` methods use these typed objects
120
120
  instead of raw Flow, Step, or RPC strings.
121
121
 
122
+ ### Waiting and map inspection
123
+
124
+ `Wait.all_of` and `Wait.any_of` may use unnamed Conditions. Every Condition in
125
+ `Wait.any_combination_of` must have a non-empty user ID; the same Condition
126
+ instance may appear in multiple combinations.
127
+
128
+ Both `Client` and `AsyncClient` provide `wait_for_attribute_equal` and
129
+ `wait_for_attribute_map_equal`. They target the current run and accept only
130
+ string, bool, int, or float wire values. JSON objects, bytes, and null fail
131
+ before transport. `AttributeMap.get_map_size/get_all_instance_keys` include
132
+ buffered sets and deletes. The matching `ChannelMap` methods are RPC-only,
133
+ include buffered publishes, and omit empty instances. Keys are decoded and
134
+ sorted. Use `force_complete_if_channels_empty(...)` for conditional completion.
135
+
122
136
  ### Errors
123
137
 
124
138
  Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
@@ -78,7 +78,7 @@ from dex.step import (
78
78
  WaitForFailurePolicy,
79
79
  dead_end,
80
80
  force_complete,
81
- force_complete_when_channels_empty,
81
+ force_complete_if_channels_empty,
82
82
  force_fail,
83
83
  go_to,
84
84
  go_to_multi,
@@ -163,7 +163,7 @@ __all__ = [
163
163
  "WorkerTarget",
164
164
  "dead_end",
165
165
  "force_complete",
166
- "force_complete_when_channels_empty",
166
+ "force_complete_if_channels_empty",
167
167
  "force_fail",
168
168
  "graceful_complete",
169
169
  "go_to",
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from enum import Enum
12
12
  from typing import Any, Sequence, TypeVar, cast
13
+ from urllib.parse import unquote
13
14
 
14
15
  from dex._utils import require_name
15
16
  from dex._value_mapper import ValueMapper
@@ -204,6 +205,38 @@ class InvocationContext:
204
205
  size=(current.size if current is not None else 0) + 1
205
206
  )
206
207
 
208
+ def _attribute_map_keys(
209
+ self,
210
+ definition: AttributeMap[object],
211
+ ) -> tuple[str, ...]:
212
+ self._require_registered(definition)
213
+ prefix = f"{definition.name}/"
214
+ physical_keys = {key for key in self._attributes if key.startswith(prefix)}
215
+ for key, write in self.attribute_writes.items():
216
+ if not key.startswith(prefix):
217
+ continue
218
+ if write.value.WhichOneof("kind") == "null_value":
219
+ physical_keys.discard(key)
220
+ else:
221
+ physical_keys.add(key)
222
+ return tuple(sorted(unquote(key[len(prefix) :]) for key in physical_keys))
223
+
224
+ def _channel_map_keys(
225
+ self,
226
+ definition: ChannelMap[object],
227
+ ) -> tuple[str, ...]:
228
+ self._require_registered(definition)
229
+ if self._method is not InvocationMethod.RPC:
230
+ raise ValueError("ChannelMap introspection requires an RPC invocation")
231
+ prefix = f"{definition.name}/"
232
+ return tuple(
233
+ sorted(
234
+ unquote(key[len(prefix) :])
235
+ for key, info in self._channel_infos.items()
236
+ if key.startswith(prefix) and info.size > 0
237
+ )
238
+ )
239
+
207
240
  def _channel_size(
208
241
  self,
209
242
  definition: Channel[object] | ChannelMap[object],
@@ -32,8 +32,6 @@ from dex.step import (
32
32
  )
33
33
  from dex.wait import Wait, WaitKind
34
34
 
35
- _INTERNAL_CONDITION_PREFIX = "__dex_internal_condition_"
36
-
37
35
 
38
36
  class WorkerDispatcher:
39
37
  def __init__(
@@ -259,7 +257,8 @@ class WorkerDispatcher:
259
257
  for combination in wait.combinations:
260
258
  waiting.condition_combinations.add(
261
259
  condition_ids=[
262
- mapper.add(condition) for condition in combination.conditions
260
+ mapper.add(condition, id_required=True)
261
+ for condition in combination.conditions
263
262
  ]
264
263
  )
265
264
  else:
@@ -417,7 +416,6 @@ class _ConditionMapper:
417
416
  self._flow = flow
418
417
  self._ids: dict[int, str] = {}
419
418
  self._used: set[str] = set()
420
- self._next_id = 0
421
419
  self.timers: list[pb.TimerCondition] = []
422
420
  self.channels: list[pb.ChannelCondition] = []
423
421
 
@@ -427,18 +425,22 @@ class _ConditionMapper:
427
425
  for condition in conditions:
428
426
  self.add(condition)
429
427
 
430
- def add(self, condition: Condition) -> str:
428
+ def add(self, condition: Condition, *, id_required: bool = False) -> str:
431
429
  identity = id(condition)
432
430
  existing = self._ids.get(identity)
433
431
  if existing is not None:
434
432
  return existing
435
- condition_id = condition.condition_id
436
- if condition_id is None:
437
- condition_id = f"{_INTERNAL_CONDITION_PREFIX}{self._next_id}"
438
- self._next_id += 1
439
- if not condition_id or condition_id in self._used:
440
- raise ValueError("duplicate or empty Condition ID")
441
- self._used.add(condition_id)
433
+ condition_id = condition.condition_id or ""
434
+ if id_required and not condition_id:
435
+ raise ValueError(
436
+ "any_combination_of requires every Condition to have an ID"
437
+ )
438
+ if condition.condition_id is not None and not condition_id:
439
+ raise ValueError("empty Condition ID")
440
+ if condition_id and condition_id in self._used:
441
+ raise ValueError("duplicate Condition ID")
442
+ if condition_id:
443
+ self._used.add(condition_id)
442
444
  if isinstance(condition, TimerCondition):
443
445
  seconds = condition.duration.total_seconds()
444
446
  if not seconds.is_integer():
@@ -787,6 +787,102 @@ class AsyncClient:
787
787
  "active",
788
788
  )
789
789
 
790
+ async def wait_for_attribute_equal(
791
+ self,
792
+ flow_id: str,
793
+ attribute: Attribute[ValueT],
794
+ expected: ValueT,
795
+ timeout: timedelta,
796
+ ) -> None:
797
+ """Await a scalar Attribute in the current run equaling ``expected``.
798
+
799
+ The Client generates a request ID. JSON, bytes, and null values raise
800
+ ``ValueError`` before transport. A remote expiry raises
801
+ ``LongPollTimeoutError``.
802
+
803
+ Args:
804
+ flow_id: The non-empty active Flow ID.
805
+ attribute: The registered scalar Attribute to observe.
806
+ expected: The string, bool, int, or float value to await.
807
+ timeout: The non-negative server-side wait duration.
808
+
809
+ Raises:
810
+ ValueError: If an identifier, timeout, or expected value is invalid.
811
+ LongPollTimeoutError: If equality is not observed before ``timeout``.
812
+ FlowNotActiveError: If the Flow closes first.
813
+ DexServiceError: If FlowService cannot perform the wait.
814
+ """
815
+ await self._wait_for_attribute_equal(
816
+ flow_id, attribute, None, expected, timeout
817
+ )
818
+
819
+ async def wait_for_attribute_map_equal(
820
+ self,
821
+ flow_id: str,
822
+ attribute: AttributeMap[ValueT],
823
+ instance: str,
824
+ expected: ValueT,
825
+ timeout: timedelta,
826
+ ) -> None:
827
+ """Await one scalar AttributeMap instance in the current run.
828
+
829
+ Scalar restrictions, request-ID generation, timeout behavior, and
830
+ service errors match :meth:`wait_for_attribute_equal`.
831
+
832
+ Args:
833
+ flow_id: The non-empty active Flow ID.
834
+ attribute: The registered AttributeMap to observe.
835
+ instance: The non-empty logical map key to observe.
836
+ expected: The string, bool, int, or float value to await.
837
+ timeout: The non-negative server-side wait duration.
838
+
839
+ Raises:
840
+ ValueError: If an identifier, timeout, or expected value is invalid.
841
+ LongPollTimeoutError: If equality is not observed before ``timeout``.
842
+ FlowNotActiveError: If the Flow closes first.
843
+ DexServiceError: If FlowService cannot perform the wait.
844
+ """
845
+ await self._wait_for_attribute_equal(
846
+ flow_id, attribute, instance, expected, timeout
847
+ )
848
+
849
+ async def _wait_for_attribute_equal(
850
+ self,
851
+ flow_id: str,
852
+ attribute: Attribute[Any] | AttributeMap[Any],
853
+ instance: str | None,
854
+ expected: object,
855
+ timeout: timedelta,
856
+ ) -> None:
857
+ encoded = self._values.encode(
858
+ expected,
859
+ self._values.codec(attribute.value_type),
860
+ )
861
+ if encoded.WhichOneof("kind") not in {
862
+ "string_value",
863
+ "bool_value",
864
+ "int_value",
865
+ "double_value",
866
+ }:
867
+ raise ValueError("wait_for_attribute_equal supports only scalar values")
868
+ await self._call(
869
+ self._service.WaitForAttribute,
870
+ pb.WaitForAttributeRequest(
871
+ flow_id=require_name(flow_id),
872
+ condition=pb.WaitForAttributeCondition(
873
+ equal=pb.WaitForAttributeEqual(
874
+ key=self._definition_name(attribute, instance),
875
+ value=encoded,
876
+ )
877
+ ),
878
+ wait_time_seconds=self._seconds32(timeout),
879
+ request_id=str(uuid4()),
880
+ ),
881
+ "wait_for_attribute_equal",
882
+ flow_id,
883
+ "active",
884
+ )
885
+
790
886
  async def update_flow_config(self, flow_id: str, config: FlowConfig) -> None:
791
887
  """Replace mutable configuration for an active Flow.
792
888
 
@@ -135,13 +135,13 @@ class AsyncWorker:
135
135
  )
136
136
  except grpc.RpcError as failure:
137
137
  self._state = "stopped"
138
- await self._flow_channel.close()
138
+ await self._flow_channel.close(None)
139
139
  self._stopped.set()
140
140
  raise RuntimeError("cannot synchronize Attribute indexes") from failure
141
141
  self._bound_port = self._server.add_insecure_port(self.options.bind_address)
142
142
  if self._bound_port == 0:
143
143
  self._state = "stopped"
144
- await self._flow_channel.close()
144
+ await self._flow_channel.close(None)
145
145
  self._stopped.set()
146
146
  raise RuntimeError(
147
147
  f"cannot bind Python AsyncWorker to {self.options.bind_address}"
@@ -164,7 +164,7 @@ class AsyncWorker:
164
164
  return
165
165
  self._state = "stopping"
166
166
  await self._server.stop(grace=5)
167
- await self._flow_channel.close()
167
+ await self._flow_channel.close(None)
168
168
  if self._state != "closed":
169
169
  self._state = "stopped"
170
170
  self._stopped.set()
@@ -206,6 +206,28 @@ class AttributeMap(Generic[ValueT]):
206
206
  """
207
207
  context._delete_attribute(cast(AttributeMap[object], self), instance)
208
208
 
209
+ def get_map_size(self, context: Context) -> int:
210
+ """Return the number of existing instances, including buffered writes.
211
+
212
+ Args:
213
+ context: The current Step or RPC Context.
214
+
215
+ Returns:
216
+ The number of keys visible after decision-local writes and deletions.
217
+ """
218
+ return len(self.get_all_instance_keys(context))
219
+
220
+ def get_all_instance_keys(self, context: Context) -> tuple[str, ...]:
221
+ """Return decoded existing instance keys in ascending order.
222
+
223
+ Args:
224
+ context: The current Step or RPC Context.
225
+
226
+ Returns:
227
+ An immutable tuple reflecting decision-local writes and deletions.
228
+ """
229
+ return context._attribute_map_keys(cast(AttributeMap[object], self))
230
+
209
231
  def lock(self, instance: str) -> AttributeLock:
210
232
  """Return a lock request for one map instance.
211
233
 
@@ -210,6 +210,28 @@ class ChannelMap(Generic[ValueT]):
210
210
  """
211
211
  return context._channel_size(cast(ChannelMap[object], self), instance)
212
212
 
213
+ def get_map_size(self, context: Context) -> int:
214
+ """Return the number of non-empty instances visible to the current RPC.
215
+
216
+ Args:
217
+ context: The current RPC Context.
218
+
219
+ Returns:
220
+ The number of keys after including publications buffered by this RPC.
221
+ """
222
+ return len(self.get_all_instance_keys(context))
223
+
224
+ def get_all_instance_keys(self, context: Context) -> tuple[str, ...]:
225
+ """Return decoded non-empty instance keys in ascending order.
226
+
227
+ Args:
228
+ context: The current RPC Context.
229
+
230
+ Returns:
231
+ An immutable tuple including publications buffered by this RPC.
232
+ """
233
+ return context._channel_map_keys(cast(ChannelMap[object], self))
234
+
213
235
  def results(self, context: Context, instance: str) -> Sequence[ValueT]:
214
236
  """Return values selected for one instance by the satisfied condition.
215
237
 
@@ -786,6 +786,99 @@ class Client:
786
786
  "active",
787
787
  )
788
788
 
789
+ def wait_for_attribute_equal(
790
+ self,
791
+ flow_id: str,
792
+ attribute: Attribute[ValueT],
793
+ expected: ValueT,
794
+ timeout: timedelta,
795
+ ) -> None:
796
+ """Wait for a scalar Attribute in the current run to equal ``expected``.
797
+
798
+ The Client generates a request ID and blocks for ``timeout``. JSON,
799
+ bytes, and null values raise ``ValueError`` before transport. A remote
800
+ expiry raises ``LongPollTimeoutError``.
801
+
802
+ Args:
803
+ flow_id: The non-empty active Flow ID.
804
+ attribute: The registered scalar Attribute to observe.
805
+ expected: The string, bool, int, or float value to await.
806
+ timeout: The non-negative server-side wait duration.
807
+
808
+ Raises:
809
+ ValueError: If an identifier, timeout, or expected value is invalid.
810
+ LongPollTimeoutError: If equality is not observed before ``timeout``.
811
+ FlowNotActiveError: If the Flow closes first.
812
+ DexServiceError: If FlowService cannot perform the wait.
813
+ """
814
+ self._wait_for_attribute_equal(flow_id, attribute, None, expected, timeout)
815
+
816
+ def wait_for_attribute_map_equal(
817
+ self,
818
+ flow_id: str,
819
+ attribute: AttributeMap[ValueT],
820
+ instance: str,
821
+ expected: ValueT,
822
+ timeout: timedelta,
823
+ ) -> None:
824
+ """Wait for one scalar AttributeMap instance in the current run.
825
+
826
+ ``instance`` is encoded as a map key. Scalar restrictions, request-ID
827
+ generation, timeout behavior, and service errors match
828
+ :meth:`wait_for_attribute_equal`.
829
+
830
+ Args:
831
+ flow_id: The non-empty active Flow ID.
832
+ attribute: The registered AttributeMap to observe.
833
+ instance: The non-empty logical map key to observe.
834
+ expected: The string, bool, int, or float value to await.
835
+ timeout: The non-negative server-side wait duration.
836
+
837
+ Raises:
838
+ ValueError: If an identifier, timeout, or expected value is invalid.
839
+ LongPollTimeoutError: If equality is not observed before ``timeout``.
840
+ FlowNotActiveError: If the Flow closes first.
841
+ DexServiceError: If FlowService cannot perform the wait.
842
+ """
843
+ self._wait_for_attribute_equal(flow_id, attribute, instance, expected, timeout)
844
+
845
+ def _wait_for_attribute_equal(
846
+ self,
847
+ flow_id: str,
848
+ attribute: Attribute[Any] | AttributeMap[Any],
849
+ instance: str | None,
850
+ expected: object,
851
+ timeout: timedelta,
852
+ ) -> None:
853
+ encoded = self._values.encode(
854
+ expected,
855
+ self._values.codec(attribute.value_type),
856
+ )
857
+ if encoded.WhichOneof("kind") not in {
858
+ "string_value",
859
+ "bool_value",
860
+ "int_value",
861
+ "double_value",
862
+ }:
863
+ raise ValueError("wait_for_attribute_equal supports only scalar values")
864
+ self._call(
865
+ self._service.WaitForAttribute,
866
+ pb.WaitForAttributeRequest(
867
+ flow_id=require_name(flow_id),
868
+ condition=pb.WaitForAttributeCondition(
869
+ equal=pb.WaitForAttributeEqual(
870
+ key=self._definition_name(attribute, instance),
871
+ value=encoded,
872
+ )
873
+ ),
874
+ wait_time_seconds=self._seconds32(timeout),
875
+ request_id=str(uuid4()),
876
+ ),
877
+ "wait_for_attribute_equal",
878
+ flow_id,
879
+ "active",
880
+ )
881
+
789
882
  def update_flow_config(self, flow_id: str, config: FlowConfig) -> None:
790
883
  """Replace mutable configuration for an active Flow.
791
884
 
@@ -149,6 +149,11 @@ class Context(Protocol):
149
149
  instance: str | None,
150
150
  ) -> None: ...
151
151
 
152
+ def _attribute_map_keys(
153
+ self,
154
+ definition: AttributeMap[object],
155
+ ) -> tuple[str, ...]: ...
156
+
152
157
  def _publish_channel(
153
158
  self,
154
159
  definition: Channel[ValueT] | ChannelMap[ValueT],
@@ -167,3 +172,8 @@ class Context(Protocol):
167
172
  definition: Channel[ValueT] | ChannelMap[ValueT],
168
173
  instance: str | None,
169
174
  ) -> Sequence[ValueT]: ...
175
+
176
+ def _channel_map_keys(
177
+ self,
178
+ definition: ChannelMap[object],
179
+ ) -> tuple[str, ...]: ...
@@ -383,7 +383,7 @@ def force_complete(output: object | None = None) -> StepDecision:
383
383
  return StepDecision(DecisionKind.FORCE_COMPLETE, output=output)
384
384
 
385
385
 
386
- def force_complete_when_channels_empty(
386
+ def force_complete_if_channels_empty(
387
387
  output: object,
388
388
  fallback: StepMovement[Any],
389
389
  *channels: Channel[Any] | ChannelMap[Any],
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "dex-python-sdk"
3
3
  # Publish uses tag sdk-python/v*; CI stamps this for release builds.
4
- version = "0.1.2"
4
+ version = "0.1.3"
5
5
  description = "Python SDK for the Dex workflow engine"
6
6
  authors = [{name = "Super Durable"}]
7
7
  readme = "README.md"
File without changes