dex-python-sdk 1.28.1__tar.gz → 1.29.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.
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/PKG-INFO +36 -7
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/README.md +35 -6
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/__init__.py +4 -0
- dex_python_sdk-1.29.0/dex/_attribute_reads.py +200 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/async_client.py +96 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/attribute.py +71 -0
- dex_python_sdk-1.29.0/dex/attribute_values.py +171 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/client.py +96 -0
- dex_python_sdk-1.29.0/dex/dexpb/dex_pb2.py +419 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/dexpb/dex_pb2.pyi +20 -4
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/flow.py +30 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/runtime_errors.py +7 -1
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/pyproject.toml +1 -1
- dex_python_sdk-1.28.1/dex/dexpb/dex_pb2.py +0 -417
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/LICENSE +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_async_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_credential_transport.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_pull_worker.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_server_protocol.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_utils.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/async_worker.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/channel.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/client_options.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/codec.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/condition.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/context.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/credential.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/flow_config.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/flow_options.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/pull_registration.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/py.typed +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/rpc_invoke_options.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/step.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/stream.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/subflow.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/timer.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/wait.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/wait_options.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/worker.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/dex/worker_options.py +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/README.md +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.0}/sdk-rust/crates/dex-blob-cache-python/README.md +0 -0
- {dex_python_sdk-1.28.1 → dex_python_sdk-1.29.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: 1.
|
|
3
|
+
Version: 1.29.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
|
|
@@ -244,10 +244,38 @@ to assess reserve pressure. It never resets and remains available after shutdown
|
|
|
244
244
|
|
|
245
245
|
## Flow state I/O
|
|
246
246
|
|
|
247
|
-
Applications
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
Applications write Flow state through typed RPCs. Client and AsyncClient do not
|
|
248
|
+
expose direct Attribute writes, Channel publication, or pending-message
|
|
249
|
+
mutation. This keeps each external state transition behind a Flow-owned method.
|
|
250
|
+
|
|
251
|
+
Read committed Attribute values without an RPC through `get_attributes`. Pass
|
|
252
|
+
one or more singleton Attributes or AttributeMap instances selected with
|
|
253
|
+
`AttributeMap.instance(key)`:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
values = client.get_attributes(flow_id, ORDER_STATUS, NOTES.instance("n1"))
|
|
257
|
+
status = values.get(ORDER_STATUS, "new")
|
|
258
|
+
if NOTES.instance("n1") in values:
|
|
259
|
+
note = values[NOTES.instance("n1")]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`AsyncClient.get_attributes` is the awaitable form. There is no single-Attribute
|
|
263
|
+
method; pass one read to read one value. The Client sends every key in one
|
|
264
|
+
request and no Worker runs, so all values come from the same committed state.
|
|
265
|
+
The call reads the current run, or the latest run when no run is open, and a
|
|
266
|
+
closed run stays readable until Dex purges it. Unlike a read inside an RPC
|
|
267
|
+
handler, it never sees writes that a running Step or RPC has not committed.
|
|
268
|
+
|
|
269
|
+
The result `AttributeValues` is keyed by the reads passed to the call, and type
|
|
270
|
+
checkers infer each value type from its key. An unset Attribute or map instance
|
|
271
|
+
is absent: `values[key]` raises `KeyError`, `values.get(key)` returns `None`,
|
|
272
|
+
`values.get(key, default)` returns `default`, and `key in values` is `False`. A
|
|
273
|
+
stored value that decodes to `None` is still present. Each read must equal a
|
|
274
|
+
definition declared by some Flow in the Client's Registry, with the same name,
|
|
275
|
+
value type, index, `sync_to_attribute_store`, and `hard_delete_on_null`;
|
|
276
|
+
otherwise the call raises `FlowDefinitionError` before contacting Dex. Two
|
|
277
|
+
reads of the same key, or no read at all, raise `ValueError`. Large values are
|
|
278
|
+
resolved through LoadBlobs and the BlobCache before decoding.
|
|
251
279
|
|
|
252
280
|
RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
|
|
253
281
|
`@rpc(is_transactional=True)` when a missing message must abort its other writes.
|
|
@@ -439,7 +467,8 @@ the Attribute or setting it to `None` permanently deletes the stored value, and
|
|
|
439
467
|
deleted value reads as absent. Only string and object values, including
|
|
440
468
|
`bytes`, are supported; `int`, `bool`, and `float` types are rejected. The
|
|
441
469
|
option cannot be combined with an index or `sync_to_attribute_store`, and
|
|
442
|
-
`wait_for_attribute_match` rejects it.
|
|
470
|
+
`wait_for_attribute_match` rejects it. `get_attributes` reads it; Dex resolves
|
|
471
|
+
the stored value. Handlers and `get_attributes` receive the value inline, and
|
|
443
472
|
the SDK never stores it in the local blob cache.
|
|
444
473
|
|
|
445
474
|
Hard-delete writes require a Server with an enabled blob store and Server
|
|
@@ -796,7 +825,7 @@ SubFlow.
|
|
|
796
825
|
### Errors
|
|
797
826
|
|
|
798
827
|
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
799
|
-
(`describe_flow`, `wait_for_flow`, and `time_travel`) raise
|
|
828
|
+
(`describe_flow`, `get_attributes`, `wait_for_flow`, and `time_travel`) raise
|
|
800
829
|
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
801
830
|
waits, config updates, and state-transfer triggers raise
|
|
802
831
|
`FlowNotActiveOrNotFoundError` when no running Flow can accept the operation.
|
|
@@ -228,10 +228,38 @@ to assess reserve pressure. It never resets and remains available after shutdown
|
|
|
228
228
|
|
|
229
229
|
## Flow state I/O
|
|
230
230
|
|
|
231
|
-
Applications
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
Applications write Flow state through typed RPCs. Client and AsyncClient do not
|
|
232
|
+
expose direct Attribute writes, Channel publication, or pending-message
|
|
233
|
+
mutation. This keeps each external state transition behind a Flow-owned method.
|
|
234
|
+
|
|
235
|
+
Read committed Attribute values without an RPC through `get_attributes`. Pass
|
|
236
|
+
one or more singleton Attributes or AttributeMap instances selected with
|
|
237
|
+
`AttributeMap.instance(key)`:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
values = client.get_attributes(flow_id, ORDER_STATUS, NOTES.instance("n1"))
|
|
241
|
+
status = values.get(ORDER_STATUS, "new")
|
|
242
|
+
if NOTES.instance("n1") in values:
|
|
243
|
+
note = values[NOTES.instance("n1")]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`AsyncClient.get_attributes` is the awaitable form. There is no single-Attribute
|
|
247
|
+
method; pass one read to read one value. The Client sends every key in one
|
|
248
|
+
request and no Worker runs, so all values come from the same committed state.
|
|
249
|
+
The call reads the current run, or the latest run when no run is open, and a
|
|
250
|
+
closed run stays readable until Dex purges it. Unlike a read inside an RPC
|
|
251
|
+
handler, it never sees writes that a running Step or RPC has not committed.
|
|
252
|
+
|
|
253
|
+
The result `AttributeValues` is keyed by the reads passed to the call, and type
|
|
254
|
+
checkers infer each value type from its key. An unset Attribute or map instance
|
|
255
|
+
is absent: `values[key]` raises `KeyError`, `values.get(key)` returns `None`,
|
|
256
|
+
`values.get(key, default)` returns `default`, and `key in values` is `False`. A
|
|
257
|
+
stored value that decodes to `None` is still present. Each read must equal a
|
|
258
|
+
definition declared by some Flow in the Client's Registry, with the same name,
|
|
259
|
+
value type, index, `sync_to_attribute_store`, and `hard_delete_on_null`;
|
|
260
|
+
otherwise the call raises `FlowDefinitionError` before contacting Dex. Two
|
|
261
|
+
reads of the same key, or no read at all, raise `ValueError`. Large values are
|
|
262
|
+
resolved through LoadBlobs and the BlobCache before decoding.
|
|
235
263
|
|
|
236
264
|
RPC handlers can stage `channel.delete(context, message_id)`. Declare the RPC as
|
|
237
265
|
`@rpc(is_transactional=True)` when a missing message must abort its other writes.
|
|
@@ -423,7 +451,8 @@ the Attribute or setting it to `None` permanently deletes the stored value, and
|
|
|
423
451
|
deleted value reads as absent. Only string and object values, including
|
|
424
452
|
`bytes`, are supported; `int`, `bool`, and `float` types are rejected. The
|
|
425
453
|
option cannot be combined with an index or `sync_to_attribute_store`, and
|
|
426
|
-
`wait_for_attribute_match` rejects it.
|
|
454
|
+
`wait_for_attribute_match` rejects it. `get_attributes` reads it; Dex resolves
|
|
455
|
+
the stored value. Handlers and `get_attributes` receive the value inline, and
|
|
427
456
|
the SDK never stores it in the local blob cache.
|
|
428
457
|
|
|
429
458
|
Hard-delete writes require a Server with an enabled blob store and Server
|
|
@@ -780,7 +809,7 @@ SubFlow.
|
|
|
780
809
|
### Errors
|
|
781
810
|
|
|
782
811
|
Client calls raise concrete `DexServiceError` subclasses. Existing-Flow reads
|
|
783
|
-
(`describe_flow`, `wait_for_flow`, and `time_travel`) raise
|
|
812
|
+
(`describe_flow`, `get_attributes`, `wait_for_flow`, and `time_travel`) raise
|
|
784
813
|
`FlowNotFoundError` when the Flow does not exist. Mutations, RPCs, timer/Step
|
|
785
814
|
waits, config updates, and state-transfer triggers raise
|
|
786
815
|
`FlowNotActiveOrNotFoundError` when no running Flow can accept the operation.
|
|
@@ -16,10 +16,12 @@ from dex.attribute import (
|
|
|
16
16
|
AttributeIndex,
|
|
17
17
|
AttributeLock,
|
|
18
18
|
AttributeMap,
|
|
19
|
+
AttributeMapInstance,
|
|
19
20
|
AttributeMapLoad,
|
|
20
21
|
IndexType,
|
|
21
22
|
)
|
|
22
23
|
from dex.attribute_match import AttributeMatch
|
|
24
|
+
from dex.attribute_values import AttributeValues
|
|
23
25
|
from dex.blob_cache import BlobCache, BlobCacheConfig, open_blob_cache
|
|
24
26
|
from dex.channel import Channel, ChannelMap, ChannelMapLoad, ChannelMessage
|
|
25
27
|
from dex.client import Client
|
|
@@ -139,9 +141,11 @@ __all__ = [
|
|
|
139
141
|
"AttributeIndex",
|
|
140
142
|
"AttributeLock",
|
|
141
143
|
"AttributeMap",
|
|
144
|
+
"AttributeMapInstance",
|
|
142
145
|
"AttributeMapLoad",
|
|
143
146
|
"AttributeMapNotLoadedError",
|
|
144
147
|
"AttributeMatch",
|
|
148
|
+
"AttributeValues",
|
|
145
149
|
"AsyncContext",
|
|
146
150
|
"AsyncClient",
|
|
147
151
|
"AsyncBufferedTextStream",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Sustainable Use License 1.0.
|
|
4
|
+
# You may not use this file except in compliance with the License.
|
|
5
|
+
# See the LICENSE file in the repository root.
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import grpc
|
|
16
|
+
|
|
17
|
+
from dex._grpc_errors import translate_rpc_error
|
|
18
|
+
from dex._value_mapper import ValueMapper
|
|
19
|
+
from dex.attribute import Attribute, AttributeMap, AttributeMapInstance
|
|
20
|
+
from dex.attribute_values import AttributeValues
|
|
21
|
+
from dex.codec import Codec
|
|
22
|
+
from dex.dexpb import dex_pb2 as pb
|
|
23
|
+
from dex.flow import Registry
|
|
24
|
+
from dex.runtime_errors import DexServiceError, FlowDefinitionError, ValueMappingError
|
|
25
|
+
|
|
26
|
+
_OPERATION = "get_attributes"
|
|
27
|
+
|
|
28
|
+
AttributeRead = Attribute[Any] | AttributeMapInstance[Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _PlannedAttributeRead:
|
|
33
|
+
read: AttributeRead
|
|
34
|
+
key: str
|
|
35
|
+
codec: Codec[Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class AttributeReadPlan:
|
|
40
|
+
"""Validated reads of one get_attributes call, shared by both Clients."""
|
|
41
|
+
|
|
42
|
+
flow_id: str
|
|
43
|
+
reads: tuple[_PlannedAttributeRead, ...]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def keys(self) -> tuple[str, ...]:
|
|
47
|
+
return tuple(planned.key for planned in self.reads)
|
|
48
|
+
|
|
49
|
+
def request(self) -> pb.GetAttributesRequest:
|
|
50
|
+
return pb.GetAttributesRequest(flow_id=self.flow_id, keys=self.keys)
|
|
51
|
+
|
|
52
|
+
def service_error(
|
|
53
|
+
self, error: grpc.RpcError, service_method: str
|
|
54
|
+
) -> DexServiceError:
|
|
55
|
+
translated = translate_rpc_error(error, _OPERATION, self.flow_id, "existing")
|
|
56
|
+
translated.args = (
|
|
57
|
+
f"{self._context()} Attributes {self._joined_keys()} {service_method} "
|
|
58
|
+
f"failed: {translated.detail}",
|
|
59
|
+
)
|
|
60
|
+
return translated
|
|
61
|
+
|
|
62
|
+
def hydration_error(self, error: Exception) -> ValueMappingError:
|
|
63
|
+
return ValueMappingError(
|
|
64
|
+
"decode",
|
|
65
|
+
f"{self._context()} Attributes {self._joined_keys()} returned a value "
|
|
66
|
+
f"that cannot be resolved: {error}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def set_entries(self, response: pb.GetAttributesResponse) -> list[pb.KV]:
|
|
70
|
+
"""Return the set entries after rejecting malformed responses.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
One entry per set read, in response order. An omitted key is unset.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If an entry has an unexpected or duplicate key, or no value.
|
|
77
|
+
"""
|
|
78
|
+
requested = set(self.keys)
|
|
79
|
+
seen: set[str] = set()
|
|
80
|
+
entries: list[pb.KV] = []
|
|
81
|
+
for entry in response.attributes:
|
|
82
|
+
if entry.key not in requested:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"{self._context()} returned unexpected Attribute {entry.key!r}"
|
|
85
|
+
)
|
|
86
|
+
if entry.key in seen:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"{self._context()} returned duplicate Attribute {entry.key!r}"
|
|
89
|
+
)
|
|
90
|
+
if not entry.HasField("value"):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"{self._context()} returned Attribute {entry.key!r} without a value"
|
|
93
|
+
)
|
|
94
|
+
seen.add(entry.key)
|
|
95
|
+
entries.append(entry)
|
|
96
|
+
return entries
|
|
97
|
+
|
|
98
|
+
def decode(
|
|
99
|
+
self,
|
|
100
|
+
values: ValueMapper,
|
|
101
|
+
entries: Sequence[pb.KV],
|
|
102
|
+
hydrated: Sequence[pb.Value],
|
|
103
|
+
) -> AttributeValues:
|
|
104
|
+
"""Decode every set value, or raise before returning any of them.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The set values keyed by the read passed to the call.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueMappingError: If a value cannot be decoded as its value type.
|
|
111
|
+
"""
|
|
112
|
+
hydrated_by_key = {
|
|
113
|
+
entry.key: value for entry, value in zip(entries, hydrated, strict=True)
|
|
114
|
+
}
|
|
115
|
+
decoded: dict[AttributeRead, object] = {}
|
|
116
|
+
for planned in self.reads:
|
|
117
|
+
value = hydrated_by_key.get(planned.key)
|
|
118
|
+
if value is None:
|
|
119
|
+
continue
|
|
120
|
+
try:
|
|
121
|
+
decoded[planned.read] = values.decode(value, planned.codec)
|
|
122
|
+
except ValueMappingError as error:
|
|
123
|
+
raise ValueMappingError(
|
|
124
|
+
"decode",
|
|
125
|
+
f"{self._context()} Attribute {planned.key!r}: {error.detail}",
|
|
126
|
+
) from error
|
|
127
|
+
return AttributeValues._of_set_values(decoded)
|
|
128
|
+
|
|
129
|
+
def _context(self) -> str:
|
|
130
|
+
return f"{_OPERATION} Flow {self.flow_id!r}"
|
|
131
|
+
|
|
132
|
+
def _joined_keys(self) -> str:
|
|
133
|
+
return ", ".join(repr(key) for key in self.keys)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def plan_attribute_reads(
|
|
137
|
+
registry: Registry,
|
|
138
|
+
values: ValueMapper,
|
|
139
|
+
flow_id: str,
|
|
140
|
+
reads: Sequence[object],
|
|
141
|
+
) -> AttributeReadPlan:
|
|
142
|
+
"""Validate the reads of one get_attributes call before any transport.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
The plan whose keys follow the order of ``reads``.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If the Flow ID is empty, no read is given, or a key repeats.
|
|
149
|
+
TypeError: If a read is not an Attribute or AttributeMapInstance.
|
|
150
|
+
FlowDefinitionError: If no registered Flow declares an equal definition.
|
|
151
|
+
"""
|
|
152
|
+
if not isinstance(flow_id, str) or not flow_id.strip():
|
|
153
|
+
raise ValueError(f"{_OPERATION} requires a non-empty Flow ID, got {flow_id!r}")
|
|
154
|
+
if not reads:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"{_OPERATION} Flow {flow_id!r} requires at least one Attribute read"
|
|
157
|
+
)
|
|
158
|
+
planned_reads: list[_PlannedAttributeRead] = []
|
|
159
|
+
seen_keys: set[str] = set()
|
|
160
|
+
for read in reads:
|
|
161
|
+
typed_read, definition, key = _definition_and_key(flow_id, read)
|
|
162
|
+
try:
|
|
163
|
+
registry._require_registered_attribute(definition)
|
|
164
|
+
except FlowDefinitionError as error:
|
|
165
|
+
raise FlowDefinitionError(
|
|
166
|
+
f"{_OPERATION} Flow {flow_id!r} Attribute {key!r}: {error}"
|
|
167
|
+
) from error
|
|
168
|
+
if key in seen_keys:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"{_OPERATION} Flow {flow_id!r} has duplicate Attribute read {key!r}"
|
|
171
|
+
)
|
|
172
|
+
seen_keys.add(key)
|
|
173
|
+
planned_reads.append(
|
|
174
|
+
_PlannedAttributeRead(
|
|
175
|
+
read=typed_read,
|
|
176
|
+
key=key,
|
|
177
|
+
codec=values.codec(definition.value_type),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return AttributeReadPlan(flow_id, tuple(planned_reads))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _definition_and_key(
|
|
184
|
+
flow_id: str,
|
|
185
|
+
read: object,
|
|
186
|
+
) -> tuple[AttributeRead, Attribute[Any] | AttributeMap[Any], str]:
|
|
187
|
+
if isinstance(read, Attribute):
|
|
188
|
+
return read, read, read.name
|
|
189
|
+
if isinstance(read, AttributeMapInstance):
|
|
190
|
+
key = Registry.physical_name(read.attribute_map.name, read.instance)
|
|
191
|
+
return read, read.attribute_map, key
|
|
192
|
+
if isinstance(read, AttributeMap):
|
|
193
|
+
raise TypeError(
|
|
194
|
+
f"{_OPERATION} Flow {flow_id!r} AttributeMap {read.name!r} needs an "
|
|
195
|
+
"instance; pass AttributeMap.instance(key)"
|
|
196
|
+
)
|
|
197
|
+
raise TypeError(
|
|
198
|
+
f"{_OPERATION} Flow {flow_id!r} read must be an Attribute or "
|
|
199
|
+
f"AttributeMapInstance, not {type(read).__name__}"
|
|
200
|
+
)
|
|
@@ -19,6 +19,7 @@ from uuid import uuid4
|
|
|
19
19
|
import grpc
|
|
20
20
|
from google.protobuf import empty_pb2
|
|
21
21
|
|
|
22
|
+
from dex._attribute_reads import plan_attribute_reads
|
|
22
23
|
from dex._credential_transport import AsyncCredentialRunner, async_channel
|
|
23
24
|
from dex._async_value_hydrator import AsyncValueHydrator
|
|
24
25
|
from dex._grpc_errors import FlowTargetRequirement, translate_rpc_error
|
|
@@ -34,12 +35,14 @@ from dex._worker_dispatcher import WorkerDispatcher
|
|
|
34
35
|
from dex.attribute import (
|
|
35
36
|
Attribute,
|
|
36
37
|
AttributeMap,
|
|
38
|
+
AttributeMapInstance,
|
|
37
39
|
_apply_attribute_store_sync,
|
|
38
40
|
_apply_hard_delete_on_null,
|
|
39
41
|
_require_attribute_wait_supported,
|
|
40
42
|
_require_matching_hard_delete_on_null,
|
|
41
43
|
)
|
|
42
44
|
from dex.attribute_match import AttributeMatch, _encode_attribute_match
|
|
45
|
+
from dex.attribute_values import AttributeValues
|
|
43
46
|
from dex.blob_cache import BlobCache
|
|
44
47
|
from dex.channel import Channel, ChannelMap
|
|
45
48
|
from dex.client_options import ClientOptions
|
|
@@ -660,6 +663,99 @@ class AsyncClient:
|
|
|
660
663
|
response.start_time.ToDatetime(tzinfo=timezone.utc),
|
|
661
664
|
)
|
|
662
665
|
|
|
666
|
+
async def get_attributes(
|
|
667
|
+
self,
|
|
668
|
+
flow_id: str,
|
|
669
|
+
*reads: Attribute[Any] | AttributeMapInstance[Any],
|
|
670
|
+
) -> AttributeValues:
|
|
671
|
+
"""Read committed Attribute values from the current or latest Flow run.
|
|
672
|
+
|
|
673
|
+
Use ``get_attributes`` to read Flow state from outside the Flow without
|
|
674
|
+
defining an RPC. Pass one or more reads. A read is a singleton
|
|
675
|
+
``Attribute`` or one AttributeMap instance selected with
|
|
676
|
+
``AttributeMap.instance(key)``. To read one Attribute, pass a single read.
|
|
677
|
+
The Client sends every key in one FlowService request, so all values come
|
|
678
|
+
from the same committed state. No Worker runs.
|
|
679
|
+
|
|
680
|
+
The call targets the current run, or the latest run when no run is open. A
|
|
681
|
+
closed run stays readable until Dex purges it. The call sees only committed
|
|
682
|
+
state. Unlike reading inside an RPC handler, it never sees writes that a
|
|
683
|
+
running Step or RPC has staged but not yet committed.
|
|
684
|
+
|
|
685
|
+
Each read must use a definition that some Flow in this Client's Registry
|
|
686
|
+
declares in its ``PersistenceSchema``. The definition matches when it is
|
|
687
|
+
equal to the registered one: same class (``Attribute`` or
|
|
688
|
+
``AttributeMap``), name, ``value_type``, ``index``,
|
|
689
|
+
``sync_to_attribute_store``, and ``hard_delete_on_null``. The definition
|
|
690
|
+
may come from any registered Flow. The server does not check that the
|
|
691
|
+
target Flow's type declares it, so a same-named Attribute of another type
|
|
692
|
+
in the target Flow fails to decode.
|
|
693
|
+
|
|
694
|
+
Attributes defined with ``hard_delete_on_null=True`` are readable; the
|
|
695
|
+
server resolves their stored values. The Client resolves other large
|
|
696
|
+
values through LoadBlobs and the Client's BlobCache, and then decodes every
|
|
697
|
+
value. On any error the call returns nothing, so no partial result is
|
|
698
|
+
visible. Every error message names the Flow ID. An error about one read or
|
|
699
|
+
value also names its Attribute name or physical key.
|
|
700
|
+
|
|
701
|
+
Example::
|
|
702
|
+
|
|
703
|
+
ORDER_STATUS = Attribute("order-status", str)
|
|
704
|
+
NOTES = AttributeMap("notes", Note)
|
|
705
|
+
|
|
706
|
+
try:
|
|
707
|
+
values = await client.get_attributes(
|
|
708
|
+
"order-42", ORDER_STATUS, NOTES.instance("n1")
|
|
709
|
+
)
|
|
710
|
+
except FlowNotFoundError:
|
|
711
|
+
return None
|
|
712
|
+
status = values.get(ORDER_STATUS, "new")
|
|
713
|
+
if NOTES.instance("n1") in values:
|
|
714
|
+
note = values[NOTES.instance("n1")]
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
flow_id: The non-empty Flow ID.
|
|
718
|
+
*reads: At least one ``Attribute`` or ``AttributeMapInstance``. Two
|
|
719
|
+
reads of the same Attribute or AttributeMap instance are rejected.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
The set values, keyed by the reads passed to this call. An Attribute or
|
|
723
|
+
AttributeMap instance without a stored value is absent from the result.
|
|
724
|
+
|
|
725
|
+
Raises:
|
|
726
|
+
ValueError: If ``flow_id`` is empty, no read is given, two reads select
|
|
727
|
+
the same Attribute or AttributeMap instance, or the response has an
|
|
728
|
+
unexpected key, a duplicate key, or an entry without a value.
|
|
729
|
+
TypeError: If a read is not an ``Attribute`` or
|
|
730
|
+
``AttributeMapInstance``, such as an ``AttributeMap`` without an
|
|
731
|
+
instance.
|
|
732
|
+
FlowDefinitionError: If no Flow in the Registry declares a matching
|
|
733
|
+
definition, including a same-named definition with another value
|
|
734
|
+
type.
|
|
735
|
+
FlowNotFoundError: If the Flow does not exist or its latest run is
|
|
736
|
+
being purged.
|
|
737
|
+
ValueMappingError: If a returned value cannot be resolved or decoded
|
|
738
|
+
as its value type. This includes an empty or non-finite value, an
|
|
739
|
+
unsupported object encoding, and a large value that LoadBlobs omits.
|
|
740
|
+
DexServiceError: If FlowService or LoadBlobs otherwise fails. A LoadBlobs
|
|
741
|
+
failure for a missing Flow raises ``FlowNotFoundError``.
|
|
742
|
+
"""
|
|
743
|
+
plan = plan_attribute_reads(self.registry, self._values, flow_id, reads)
|
|
744
|
+
try:
|
|
745
|
+
response = await self._service.GetAttributes(plan.request())
|
|
746
|
+
except grpc.RpcError as error:
|
|
747
|
+
raise plan.service_error(error, "GetAttributes") from error
|
|
748
|
+
entries = plan.set_entries(response)
|
|
749
|
+
try:
|
|
750
|
+
hydrated = await self._hydrator.hydrate_all(
|
|
751
|
+
flow_id, [entry.value for entry in entries]
|
|
752
|
+
)
|
|
753
|
+
except grpc.RpcError as error:
|
|
754
|
+
raise plan.service_error(error, "LoadBlobs") from error
|
|
755
|
+
except (ValueError, RuntimeError) as error:
|
|
756
|
+
raise plan.hydration_error(error) from error
|
|
757
|
+
return plan.decode(self._values, entries, hydrated)
|
|
758
|
+
|
|
663
759
|
async def search_flows(
|
|
664
760
|
self,
|
|
665
761
|
query: str,
|
|
@@ -378,6 +378,37 @@ class AttributeMap(Generic[ValueT]):
|
|
|
378
378
|
"""
|
|
379
379
|
return AttributeMapLoad(self, require_map_instance(instance))
|
|
380
380
|
|
|
381
|
+
def instance(self, instance: str) -> AttributeMapInstance[ValueT]:
|
|
382
|
+
"""Select one typed instance for a Client Attribute read.
|
|
383
|
+
|
|
384
|
+
Pass the result to ``Client.get_attributes`` or
|
|
385
|
+
``AsyncClient.get_attributes``, and use the same selector, or an equal
|
|
386
|
+
one, to look up the value in the returned ``AttributeValues``. The
|
|
387
|
+
selector keeps the AttributeMap value type, so type checkers infer the
|
|
388
|
+
value type of the lookup.
|
|
389
|
+
|
|
390
|
+
This differs from :meth:`load`. ``load`` selects an instance that an RPC
|
|
391
|
+
handler receives in its snapshot. ``instance`` selects an instance that
|
|
392
|
+
a Client reads from outside the Flow.
|
|
393
|
+
|
|
394
|
+
Example::
|
|
395
|
+
|
|
396
|
+
notes = AttributeMap("notes", Note)
|
|
397
|
+
values = client.get_attributes("order-42", notes.instance("n1"))
|
|
398
|
+
note = values.get(notes.instance("n1"))
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
instance: The non-empty, slash-free logical map key to read.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
A typed selector for this one instance.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
TypeError: If ``instance`` is not a string.
|
|
408
|
+
ValueError: If ``instance`` is empty or contains ``/``.
|
|
409
|
+
"""
|
|
410
|
+
return AttributeMapInstance(self, instance)
|
|
411
|
+
|
|
381
412
|
def get(self, context: Context, instance: str) -> ValueT:
|
|
382
413
|
"""Return one map instance from a Step or RPC Context.
|
|
383
414
|
|
|
@@ -451,6 +482,46 @@ class AttributeMap(Generic[ValueT]):
|
|
|
451
482
|
return AttributeLock(self, instance)
|
|
452
483
|
|
|
453
484
|
|
|
485
|
+
@dataclass(frozen=True)
|
|
486
|
+
class AttributeMapInstance(Generic[ValueT]):
|
|
487
|
+
"""Select one typed AttributeMap instance for a Client Attribute read.
|
|
488
|
+
|
|
489
|
+
Create a selector with :meth:`AttributeMap.instance`. Pass it to
|
|
490
|
+
``Client.get_attributes`` or ``AsyncClient.get_attributes`` together with
|
|
491
|
+
singleton Attributes. Then use it as the key of the returned
|
|
492
|
+
``AttributeValues``. Selectors are immutable and hashable. Two selectors are
|
|
493
|
+
equal when their AttributeMap definitions are equal and their instance keys
|
|
494
|
+
are the same string.
|
|
495
|
+
|
|
496
|
+
Example::
|
|
497
|
+
|
|
498
|
+
notes = AttributeMap("notes", Note)
|
|
499
|
+
first_note = notes.instance("n1")
|
|
500
|
+
values = client.get_attributes("order-42", first_note)
|
|
501
|
+
if first_note in values:
|
|
502
|
+
print(values[first_note].text)
|
|
503
|
+
|
|
504
|
+
Attributes:
|
|
505
|
+
attribute_map: The AttributeMap definition that owns the instance.
|
|
506
|
+
instance: The non-empty logical map key. It must not contain ``/``.
|
|
507
|
+
|
|
508
|
+
Raises:
|
|
509
|
+
TypeError: If ``attribute_map`` is not an AttributeMap, or ``instance`` is
|
|
510
|
+
not a string.
|
|
511
|
+
ValueError: If ``instance`` is empty or contains ``/``.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
attribute_map: AttributeMap[ValueT]
|
|
515
|
+
instance: str
|
|
516
|
+
|
|
517
|
+
def __post_init__(self) -> None:
|
|
518
|
+
if not isinstance(self.attribute_map, AttributeMap):
|
|
519
|
+
raise TypeError("AttributeMapInstance requires an AttributeMap definition")
|
|
520
|
+
if not isinstance(self.instance, str):
|
|
521
|
+
raise TypeError("attribute-map instance must be a string")
|
|
522
|
+
require_map_instance(self.instance)
|
|
523
|
+
|
|
524
|
+
|
|
454
525
|
@dataclass(frozen=True)
|
|
455
526
|
class AttributeLock:
|
|
456
527
|
"""Describe an Attribute lock acquired for a Step or RPC handler.
|