dex-python-sdk 0.7.0__tar.gz → 0.8.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-0.7.0 → dex_python_sdk-0.8.0}/PKG-INFO +5 -2
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/README.md +4 -1
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_async_value_hydrator.py +54 -17
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_value_hydrator.py +41 -17
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_value_mapper.py +5 -6
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/async_client.py +13 -3
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/client.py +13 -3
- dex_python_sdk-0.8.0/dex/dexpb/dex_pb2.py +503 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/dexpb/dex_pb2.pyi +12 -4
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/wait.py +6 -1
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/wait_options.py +16 -6
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/pyproject.toml +1 -1
- dex_python_sdk-0.7.0/dex/dexpb/dex_pb2.py +0 -501
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/LICENSE +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/__init__.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_async_worker_dispatcher.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_utils.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_worker_dispatcher.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/attribute.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/channel.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/client_options.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/codec.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/condition.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/context.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/flow.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/flow_options.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/py.typed +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/step.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/stream.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/subflow.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/timer.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/worker.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.7.0 → dex_python_sdk-0.8.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.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -455,7 +455,10 @@ client operations, and assertions as the Java suite against an isolated
|
|
|
455
455
|
The strongly typed contracts, registry, synchronous Client/Worker, optional
|
|
456
456
|
`AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
|
|
457
457
|
implemented. Python owns its gRPC transport; the native bridge is limited to
|
|
458
|
-
the shared BlobCache.
|
|
458
|
+
the shared BlobCache. Serialized object values use wire encoding `json` for JSON
|
|
459
|
+
and `raw` for raw bytes. Internal Blob references are opaque. Hydration sends the
|
|
460
|
+
owning Flow ID with each reference, and the cache is isolated by
|
|
461
|
+
`(flow_id, blob_ref)`. Design notes:
|
|
459
462
|
[`python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md) and
|
|
460
463
|
[`python-sdk-step-streaming.md`](../docs/design/plan/python-sdk-step-streaming.md).
|
|
461
464
|
|
|
@@ -439,7 +439,10 @@ client operations, and assertions as the Java suite against an isolated
|
|
|
439
439
|
The strongly typed contracts, registry, synchronous Client/Worker, optional
|
|
440
440
|
`AsyncClient`/`AsyncWorker` (`grpc.aio`), and Rust-backed BlobCache are
|
|
441
441
|
implemented. Python owns its gRPC transport; the native bridge is limited to
|
|
442
|
-
the shared BlobCache.
|
|
442
|
+
the shared BlobCache. Serialized object values use wire encoding `json` for JSON
|
|
443
|
+
and `raw` for raw bytes. Internal Blob references are opaque. Hydration sends the
|
|
444
|
+
owning Flow ID with each reference, and the cache is isolated by
|
|
445
|
+
`(flow_id, blob_ref)`. Design notes:
|
|
443
446
|
[`python-sdk-async-apis.md`](../docs/design/plan/python-sdk-async-apis.md) and
|
|
444
447
|
[`python-sdk-step-streaming.md`](../docs/design/plan/python-sdk-step-streaming.md).
|
|
445
448
|
|
|
@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
@dataclass
|
|
22
22
|
class _PendingBlob:
|
|
23
|
+
flow_id: str
|
|
23
24
|
blob_id: str
|
|
24
25
|
is_object: bool
|
|
25
26
|
request: pb.Value
|
|
@@ -36,21 +37,37 @@ class AsyncValueHydrator:
|
|
|
36
37
|
self._service = service
|
|
37
38
|
self._cache = cache
|
|
38
39
|
|
|
39
|
-
async def hydrate(self, value: pb.Value) -> pb.Value:
|
|
40
|
-
return (await self.hydrate_all([value]))[0]
|
|
40
|
+
async def hydrate(self, flow_id: str, value: pb.Value) -> pb.Value:
|
|
41
|
+
return (await self.hydrate_all(flow_id, [value]))[0]
|
|
41
42
|
|
|
42
|
-
async def hydrate_all(
|
|
43
|
+
async def hydrate_all(
|
|
44
|
+
self,
|
|
45
|
+
flow_id: str,
|
|
46
|
+
values: list[pb.Value],
|
|
47
|
+
) -> list[pb.Value]:
|
|
48
|
+
return await self.hydrate_all_for_flows(
|
|
49
|
+
[(flow_id, value) for value in values]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def hydrate_all_for_flows(
|
|
53
|
+
self,
|
|
54
|
+
flow_values: list[tuple[str, pb.Value]],
|
|
55
|
+
) -> list[pb.Value]:
|
|
56
|
+
values = [value for _, value in flow_values]
|
|
43
57
|
hydrated = list(values)
|
|
44
|
-
pending: dict[tuple[str, bool], _PendingBlob] = {}
|
|
45
|
-
for index, value in enumerate(
|
|
58
|
+
pending: dict[tuple[str, str, bool], _PendingBlob] = {}
|
|
59
|
+
for index, (flow_id, value) in enumerate(flow_values):
|
|
60
|
+
if not flow_id:
|
|
61
|
+
raise ValueError(f"Flow ID at index {index} is required")
|
|
46
62
|
key = self._blob_key(value)
|
|
47
63
|
if key is None:
|
|
48
64
|
self._validate_concrete(value)
|
|
49
65
|
continue
|
|
50
|
-
|
|
66
|
+
flow_blob_key = (flow_id, key[0], key[1])
|
|
67
|
+
blob = pending.get(flow_blob_key)
|
|
51
68
|
if blob is None:
|
|
52
|
-
blob = _PendingBlob(key[0], key[1], value)
|
|
53
|
-
pending[
|
|
69
|
+
blob = _PendingBlob(flow_id, key[0], key[1], value)
|
|
70
|
+
pending[flow_blob_key] = blob
|
|
54
71
|
blob.indexes.append(index)
|
|
55
72
|
|
|
56
73
|
misses: list[_PendingBlob] = []
|
|
@@ -86,7 +103,7 @@ class AsyncValueHydrator:
|
|
|
86
103
|
for message in channel_values.messages
|
|
87
104
|
]
|
|
88
105
|
values.extend(message.value for message in channel_messages)
|
|
89
|
-
hydrated = iter(await self.hydrate_all(values))
|
|
106
|
+
hydrated = iter(await self.hydrate_all(request.context.flow_id, values))
|
|
90
107
|
result.step_input.CopyFrom(next(hydrated))
|
|
91
108
|
if has_heartbeat:
|
|
92
109
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
@@ -122,7 +139,11 @@ class AsyncValueHydrator:
|
|
|
122
139
|
values.extend(message.value for message in channel_messages)
|
|
123
140
|
for channel_result in request.condition_results.channel_results:
|
|
124
141
|
values.extend(channel_result.values)
|
|
125
|
-
|
|
142
|
+
for flow_result in request.condition_results.sub_flow_results:
|
|
143
|
+
values.extend(
|
|
144
|
+
completion.completed_step_output for completion in flow_result.results
|
|
145
|
+
)
|
|
146
|
+
hydrated = iter(await self.hydrate_all(request.context.flow_id, values))
|
|
126
147
|
if has_step_input:
|
|
127
148
|
result.step_input.CopyFrom(next(hydrated))
|
|
128
149
|
if has_heartbeat:
|
|
@@ -141,6 +162,9 @@ class AsyncValueHydrator:
|
|
|
141
162
|
for channel_result in result.condition_results.channel_results:
|
|
142
163
|
for value in channel_result.values:
|
|
143
164
|
value.CopyFrom(next(hydrated))
|
|
165
|
+
for flow_result in result.condition_results.sub_flow_results:
|
|
166
|
+
for completion in flow_result.results:
|
|
167
|
+
completion.completed_step_output.CopyFrom(next(hydrated))
|
|
144
168
|
return result
|
|
145
169
|
|
|
146
170
|
async def rpc_request(
|
|
@@ -156,7 +180,7 @@ class AsyncValueHydrator:
|
|
|
156
180
|
for message in channel_values.messages
|
|
157
181
|
]
|
|
158
182
|
values.extend(message.value for message in channel_messages)
|
|
159
|
-
hydrated = await self.hydrate_all(values)
|
|
183
|
+
hydrated = await self.hydrate_all(request.context.flow_id, values)
|
|
160
184
|
result.input.CopyFrom(hydrated[0])
|
|
161
185
|
hydrated_entries = iter(hydrated[1:])
|
|
162
186
|
for entry in result.attributes:
|
|
@@ -172,10 +196,11 @@ class AsyncValueHydrator:
|
|
|
172
196
|
|
|
173
197
|
async def step_outputs(
|
|
174
198
|
self,
|
|
199
|
+
flow_id: str,
|
|
175
200
|
outputs: list[pb.StepCompletionOutput],
|
|
176
201
|
) -> list[pb.StepCompletionOutput]:
|
|
177
202
|
values = [output.completed_step_output for output in outputs]
|
|
178
|
-
hydrated = await self.hydrate_all(values)
|
|
203
|
+
hydrated = await self.hydrate_all(flow_id, values)
|
|
179
204
|
results: list[pb.StepCompletionOutput] = []
|
|
180
205
|
for output, value in zip(outputs, hydrated):
|
|
181
206
|
result = pb.StepCompletionOutput()
|
|
@@ -188,7 +213,15 @@ class AsyncValueHydrator:
|
|
|
188
213
|
if not misses:
|
|
189
214
|
return
|
|
190
215
|
response = await self._service.LoadBlobs(
|
|
191
|
-
pb.LoadBlobsRequest(
|
|
216
|
+
pb.LoadBlobsRequest(
|
|
217
|
+
entries=[
|
|
218
|
+
pb.LoadBlobRequestEntry(
|
|
219
|
+
flow_id=miss.flow_id,
|
|
220
|
+
blob_value=miss.request,
|
|
221
|
+
)
|
|
222
|
+
for miss in misses
|
|
223
|
+
]
|
|
224
|
+
)
|
|
192
225
|
)
|
|
193
226
|
for miss in misses:
|
|
194
227
|
concrete = response.values.get(miss.blob_id)
|
|
@@ -200,7 +233,7 @@ class AsyncValueHydrator:
|
|
|
200
233
|
|
|
201
234
|
def _read_cache(self, blob: _PendingBlob) -> pb.Value | None:
|
|
202
235
|
try:
|
|
203
|
-
payload = self._cache.get(blob
|
|
236
|
+
payload = self._cache.get(self._cache_key(blob))
|
|
204
237
|
if payload is None:
|
|
205
238
|
return None
|
|
206
239
|
if blob.is_object:
|
|
@@ -212,7 +245,7 @@ class AsyncValueHydrator:
|
|
|
212
245
|
except Exception:
|
|
213
246
|
_LOGGER.warning("cannot read cached blob %s", blob.blob_id, exc_info=True)
|
|
214
247
|
try:
|
|
215
|
-
self._cache.delete(blob
|
|
248
|
+
self._cache.delete(self._cache_key(blob))
|
|
216
249
|
except Exception:
|
|
217
250
|
_LOGGER.warning(
|
|
218
251
|
"cannot delete cached blob %s",
|
|
@@ -228,7 +261,7 @@ class AsyncValueHydrator:
|
|
|
228
261
|
if blob.is_object
|
|
229
262
|
else concrete.string_value.encode("utf-8")
|
|
230
263
|
)
|
|
231
|
-
self._cache.put(blob
|
|
264
|
+
self._cache.put(self._cache_key(blob), payload)
|
|
232
265
|
except Exception:
|
|
233
266
|
_LOGGER.warning("cannot cache blob %s", blob.blob_id, exc_info=True)
|
|
234
267
|
|
|
@@ -247,6 +280,10 @@ class AsyncValueHydrator:
|
|
|
247
280
|
return blob_id, True
|
|
248
281
|
return None
|
|
249
282
|
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _cache_key(blob: _PendingBlob) -> str:
|
|
285
|
+
return f"{len(blob.flow_id)}:{blob.flow_id}{blob.blob_id}"
|
|
286
|
+
|
|
250
287
|
@staticmethod
|
|
251
288
|
def _validate_hydrated(blob: _PendingBlob, value: pb.Value) -> None:
|
|
252
289
|
expected = "obj_value" if blob.is_object else "string_value"
|
|
@@ -268,7 +305,7 @@ class AsyncValueHydrator:
|
|
|
268
305
|
raise ValueError("non-finite numbers are unsupported")
|
|
269
306
|
return
|
|
270
307
|
if kind == "obj_value":
|
|
271
|
-
if value.obj_value.encoding not in ("json", "
|
|
308
|
+
if value.obj_value.encoding not in ("json", "raw"):
|
|
272
309
|
raise ValueError(
|
|
273
310
|
f"unsupported object encoding {value.obj_value.encoding}"
|
|
274
311
|
)
|
|
@@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
@dataclass
|
|
22
22
|
class _PendingBlob:
|
|
23
|
+
flow_id: str
|
|
23
24
|
blob_id: str
|
|
24
25
|
is_object: bool
|
|
25
26
|
request: pb.Value
|
|
@@ -36,21 +37,31 @@ class ValueHydrator:
|
|
|
36
37
|
self._service = service
|
|
37
38
|
self._cache = cache
|
|
38
39
|
|
|
39
|
-
def hydrate(self, value: pb.Value) -> pb.Value:
|
|
40
|
-
return self.hydrate_all([value])[0]
|
|
40
|
+
def hydrate(self, flow_id: str, value: pb.Value) -> pb.Value:
|
|
41
|
+
return self.hydrate_all(flow_id, [value])[0]
|
|
41
42
|
|
|
42
|
-
def hydrate_all(self, values: list[pb.Value]) -> list[pb.Value]:
|
|
43
|
+
def hydrate_all(self, flow_id: str, values: list[pb.Value]) -> list[pb.Value]:
|
|
44
|
+
return self.hydrate_all_for_flows([(flow_id, value) for value in values])
|
|
45
|
+
|
|
46
|
+
def hydrate_all_for_flows(
|
|
47
|
+
self,
|
|
48
|
+
flow_values: list[tuple[str, pb.Value]],
|
|
49
|
+
) -> list[pb.Value]:
|
|
50
|
+
values = [value for _, value in flow_values]
|
|
43
51
|
hydrated = list(values)
|
|
44
|
-
pending: dict[tuple[str, bool], _PendingBlob] = {}
|
|
45
|
-
for index, value in enumerate(
|
|
52
|
+
pending: dict[tuple[str, str, bool], _PendingBlob] = {}
|
|
53
|
+
for index, (flow_id, value) in enumerate(flow_values):
|
|
54
|
+
if not flow_id:
|
|
55
|
+
raise ValueError(f"Flow ID at index {index} is required")
|
|
46
56
|
key = self._blob_key(value)
|
|
47
57
|
if key is None:
|
|
48
58
|
self._validate_concrete(value)
|
|
49
59
|
continue
|
|
50
|
-
|
|
60
|
+
flow_blob_key = (flow_id, key[0], key[1])
|
|
61
|
+
blob = pending.get(flow_blob_key)
|
|
51
62
|
if blob is None:
|
|
52
|
-
blob = _PendingBlob(key[0], key[1], value)
|
|
53
|
-
pending[
|
|
63
|
+
blob = _PendingBlob(flow_id, key[0], key[1], value)
|
|
64
|
+
pending[flow_blob_key] = blob
|
|
54
65
|
blob.indexes.append(index)
|
|
55
66
|
|
|
56
67
|
misses: list[_PendingBlob] = []
|
|
@@ -86,7 +97,7 @@ class ValueHydrator:
|
|
|
86
97
|
for message in channel_values.messages
|
|
87
98
|
]
|
|
88
99
|
values.extend(message.value for message in channel_messages)
|
|
89
|
-
hydrated = iter(self.hydrate_all(values))
|
|
100
|
+
hydrated = iter(self.hydrate_all(request.context.flow_id, values))
|
|
90
101
|
result.step_input.CopyFrom(next(hydrated))
|
|
91
102
|
if has_heartbeat:
|
|
92
103
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
@@ -126,7 +137,7 @@ class ValueHydrator:
|
|
|
126
137
|
values.extend(
|
|
127
138
|
completion.completed_step_output for completion in flow_result.results
|
|
128
139
|
)
|
|
129
|
-
hydrated = iter(self.hydrate_all(values))
|
|
140
|
+
hydrated = iter(self.hydrate_all(request.context.flow_id, values))
|
|
130
141
|
if has_step_input:
|
|
131
142
|
result.step_input.CopyFrom(next(hydrated))
|
|
132
143
|
if has_heartbeat:
|
|
@@ -163,7 +174,7 @@ class ValueHydrator:
|
|
|
163
174
|
for message in channel_values.messages
|
|
164
175
|
]
|
|
165
176
|
values.extend(message.value for message in channel_messages)
|
|
166
|
-
hydrated = self.hydrate_all(values)
|
|
177
|
+
hydrated = self.hydrate_all(request.context.flow_id, values)
|
|
167
178
|
result.input.CopyFrom(hydrated[0])
|
|
168
179
|
hydrated_entries = iter(hydrated[1:])
|
|
169
180
|
for entry in result.attributes:
|
|
@@ -179,10 +190,11 @@ class ValueHydrator:
|
|
|
179
190
|
|
|
180
191
|
def step_outputs(
|
|
181
192
|
self,
|
|
193
|
+
flow_id: str,
|
|
182
194
|
outputs: list[pb.StepCompletionOutput],
|
|
183
195
|
) -> list[pb.StepCompletionOutput]:
|
|
184
196
|
values = [output.completed_step_output for output in outputs]
|
|
185
|
-
hydrated = self.hydrate_all(values)
|
|
197
|
+
hydrated = self.hydrate_all(flow_id, values)
|
|
186
198
|
results: list[pb.StepCompletionOutput] = []
|
|
187
199
|
for output, value in zip(outputs, hydrated):
|
|
188
200
|
result = pb.StepCompletionOutput()
|
|
@@ -195,7 +207,15 @@ class ValueHydrator:
|
|
|
195
207
|
if not misses:
|
|
196
208
|
return
|
|
197
209
|
response = self._service.LoadBlobs(
|
|
198
|
-
pb.LoadBlobsRequest(
|
|
210
|
+
pb.LoadBlobsRequest(
|
|
211
|
+
entries=[
|
|
212
|
+
pb.LoadBlobRequestEntry(
|
|
213
|
+
flow_id=miss.flow_id,
|
|
214
|
+
blob_value=miss.request,
|
|
215
|
+
)
|
|
216
|
+
for miss in misses
|
|
217
|
+
]
|
|
218
|
+
)
|
|
199
219
|
)
|
|
200
220
|
for miss in misses:
|
|
201
221
|
concrete = response.values.get(miss.blob_id)
|
|
@@ -207,7 +227,7 @@ class ValueHydrator:
|
|
|
207
227
|
|
|
208
228
|
def _read_cache(self, blob: _PendingBlob) -> pb.Value | None:
|
|
209
229
|
try:
|
|
210
|
-
payload = self._cache.get(blob
|
|
230
|
+
payload = self._cache.get(self._cache_key(blob))
|
|
211
231
|
if payload is None:
|
|
212
232
|
return None
|
|
213
233
|
if blob.is_object:
|
|
@@ -219,7 +239,7 @@ class ValueHydrator:
|
|
|
219
239
|
except Exception:
|
|
220
240
|
_LOGGER.warning("cannot read cached blob %s", blob.blob_id, exc_info=True)
|
|
221
241
|
try:
|
|
222
|
-
self._cache.delete(blob
|
|
242
|
+
self._cache.delete(self._cache_key(blob))
|
|
223
243
|
except Exception:
|
|
224
244
|
_LOGGER.warning(
|
|
225
245
|
"cannot delete cached blob %s",
|
|
@@ -235,7 +255,7 @@ class ValueHydrator:
|
|
|
235
255
|
if blob.is_object
|
|
236
256
|
else concrete.string_value.encode("utf-8")
|
|
237
257
|
)
|
|
238
|
-
self._cache.put(blob
|
|
258
|
+
self._cache.put(self._cache_key(blob), payload)
|
|
239
259
|
except Exception:
|
|
240
260
|
_LOGGER.warning("cannot cache blob %s", blob.blob_id, exc_info=True)
|
|
241
261
|
|
|
@@ -254,6 +274,10 @@ class ValueHydrator:
|
|
|
254
274
|
return blob_id, True
|
|
255
275
|
return None
|
|
256
276
|
|
|
277
|
+
@staticmethod
|
|
278
|
+
def _cache_key(blob: _PendingBlob) -> str:
|
|
279
|
+
return f"{len(blob.flow_id)}:{blob.flow_id}{blob.blob_id}"
|
|
280
|
+
|
|
257
281
|
@staticmethod
|
|
258
282
|
def _validate_hydrated(blob: _PendingBlob, value: pb.Value) -> None:
|
|
259
283
|
expected = "obj_value" if blob.is_object else "string_value"
|
|
@@ -275,7 +299,7 @@ class ValueHydrator:
|
|
|
275
299
|
raise ValueError("non-finite numbers are unsupported")
|
|
276
300
|
return
|
|
277
301
|
if kind == "obj_value":
|
|
278
|
-
if value.obj_value.encoding not in ("json", "
|
|
302
|
+
if value.obj_value.encoding not in ("json", "raw"):
|
|
279
303
|
raise ValueError(
|
|
280
304
|
f"unsupported object encoding {value.obj_value.encoding}"
|
|
281
305
|
)
|
|
@@ -22,7 +22,7 @@ from dex.dexpb import dex_pb2 as pb
|
|
|
22
22
|
from dex.runtime_errors import ValueMappingError
|
|
23
23
|
|
|
24
24
|
_JSON_ENCODING = "json"
|
|
25
|
-
_RAW_BYTES_ENCODING = "
|
|
25
|
+
_RAW_BYTES_ENCODING = "raw"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class ValueMapper:
|
|
@@ -43,10 +43,7 @@ class ValueMapper:
|
|
|
43
43
|
def _encode(self, value: Any, codec: Codec[Any]) -> pb.Value:
|
|
44
44
|
if value is None:
|
|
45
45
|
return pb.Value(
|
|
46
|
-
|
|
47
|
-
encoding=_JSON_ENCODING,
|
|
48
|
-
payload=b"null",
|
|
49
|
-
)
|
|
46
|
+
null_value=cast(struct_pb2.NullValue, struct_pb2.NULL_VALUE)
|
|
50
47
|
)
|
|
51
48
|
logical = codec.encode(value)
|
|
52
49
|
if logical.kind is WireKind.STRING:
|
|
@@ -134,7 +131,7 @@ class ValueMapper:
|
|
|
134
131
|
):
|
|
135
132
|
raise ValueError("blob-backed Value was not hydrated")
|
|
136
133
|
if kind == "null_value":
|
|
137
|
-
|
|
134
|
+
return None
|
|
138
135
|
raise TypeError(f"cannot decode {kind or 'empty Value'} as {codec.type_name}")
|
|
139
136
|
|
|
140
137
|
def to_value(self, value: pb.Value) -> Value:
|
|
@@ -166,6 +163,8 @@ class ValueMapper:
|
|
|
166
163
|
"internal_blob_id_for_obj_value",
|
|
167
164
|
):
|
|
168
165
|
raise ValueError("blob-backed Value was not hydrated")
|
|
166
|
+
if kind == "null_value":
|
|
167
|
+
return Value(WireKind.JSON, "null")
|
|
169
168
|
raise TypeError(f"cannot wrap {kind or 'empty Value'} as Value")
|
|
170
169
|
|
|
171
170
|
@staticmethod
|
|
@@ -308,7 +308,7 @@ class AsyncClient:
|
|
|
308
308
|
if rpc.output_codec is None:
|
|
309
309
|
return None
|
|
310
310
|
return self._values.decode(
|
|
311
|
-
await self._hydrator.hydrate(response.output),
|
|
311
|
+
await self._hydrator.hydrate(flow_id, response.output),
|
|
312
312
|
rpc.output_codec,
|
|
313
313
|
)
|
|
314
314
|
|
|
@@ -505,7 +505,7 @@ class AsyncClient:
|
|
|
505
505
|
DexServiceError: If FlowService cannot perform the wait.
|
|
506
506
|
"""
|
|
507
507
|
response = await self._wait_for_flow_response(flow_id, timeout)
|
|
508
|
-
hydrated = await self._hydrator.step_outputs(list(response.results))
|
|
508
|
+
hydrated = await self._hydrator.step_outputs(flow_id, list(response.results))
|
|
509
509
|
mapped = pb.FlowResult()
|
|
510
510
|
mapped.CopyFrom(response)
|
|
511
511
|
del mapped.results[:]
|
|
@@ -625,7 +625,9 @@ class AsyncClient:
|
|
|
625
625
|
self, entry: pb.SearchFlowsResponseEntry
|
|
626
626
|
) -> SearchFlowEntry:
|
|
627
627
|
attributes = {
|
|
628
|
-
kv.key: self._values.to_value(
|
|
628
|
+
kv.key: self._values.to_value(
|
|
629
|
+
await self._hydrator.hydrate(entry.flow_id, kv.value)
|
|
630
|
+
)
|
|
629
631
|
for kv in entry.indexed_attributes
|
|
630
632
|
}
|
|
631
633
|
closed_at = (
|
|
@@ -734,6 +736,10 @@ class AsyncClient:
|
|
|
734
736
|
|
|
735
737
|
The server derives a stable Request ID from the Step execution when none
|
|
736
738
|
is supplied. Transport long polls automatically reattach to the same wait.
|
|
739
|
+
Leave the maximum wait time at zero for ordinary infinite waits. A positive
|
|
740
|
+
value releases per-Flow in-flight Update capacity for abandoned waits.
|
|
741
|
+
The budget spans Continue-as-New and is distinct from the caller's lifetime.
|
|
742
|
+
Retrying after expiry creates a new Update generation.
|
|
737
743
|
|
|
738
744
|
Args:
|
|
739
745
|
flow_id: The non-empty active Flow ID.
|
|
@@ -780,6 +786,10 @@ class AsyncClient:
|
|
|
780
786
|
|
|
781
787
|
The Client returns the value observed by the successful wait. The server
|
|
782
788
|
derives a stable Request ID from the condition when none is supplied.
|
|
789
|
+
Leave the maximum wait time at zero for ordinary infinite waits. A positive
|
|
790
|
+
value releases per-Flow in-flight Update capacity for rarely matching waits.
|
|
791
|
+
The budget spans transport reattachments and Continue-as-New, and is distinct
|
|
792
|
+
from the caller's lifetime. Retrying after expiry creates a new Update generation.
|
|
783
793
|
JSON, bytes, and null operands raise ``ValueError`` before transport.
|
|
784
794
|
|
|
785
795
|
Args:
|
|
@@ -308,7 +308,7 @@ class Client:
|
|
|
308
308
|
if rpc.output_codec is None:
|
|
309
309
|
return None
|
|
310
310
|
return self._values.decode(
|
|
311
|
-
self._hydrator.hydrate(response.output),
|
|
311
|
+
self._hydrator.hydrate(flow_id, response.output),
|
|
312
312
|
rpc.output_codec,
|
|
313
313
|
)
|
|
314
314
|
|
|
@@ -505,7 +505,7 @@ class Client:
|
|
|
505
505
|
DexServiceError: If FlowService cannot perform the wait.
|
|
506
506
|
"""
|
|
507
507
|
response = self._wait_for_flow_response(flow_id, timeout)
|
|
508
|
-
hydrated = self._hydrator.step_outputs(list(response.results))
|
|
508
|
+
hydrated = self._hydrator.step_outputs(flow_id, list(response.results))
|
|
509
509
|
mapped = pb.FlowResult()
|
|
510
510
|
mapped.CopyFrom(response)
|
|
511
511
|
del mapped.results[:]
|
|
@@ -624,7 +624,9 @@ class Client:
|
|
|
624
624
|
|
|
625
625
|
def _map_search_entry(self, entry: pb.SearchFlowsResponseEntry) -> SearchFlowEntry:
|
|
626
626
|
attributes = {
|
|
627
|
-
kv.key: self._values.to_value(
|
|
627
|
+
kv.key: self._values.to_value(
|
|
628
|
+
self._hydrator.hydrate(entry.flow_id, kv.value)
|
|
629
|
+
)
|
|
628
630
|
for kv in entry.indexed_attributes
|
|
629
631
|
}
|
|
630
632
|
closed_at = (
|
|
@@ -731,6 +733,10 @@ class Client:
|
|
|
731
733
|
|
|
732
734
|
The server derives a stable Request ID from the Step execution when none
|
|
733
735
|
is supplied. Transport long polls automatically reattach to the same wait.
|
|
736
|
+
Leave the maximum wait time at zero for ordinary infinite waits. A positive
|
|
737
|
+
value releases per-Flow in-flight Update capacity for abandoned waits.
|
|
738
|
+
The budget spans Continue-as-New and is distinct from the caller's lifetime.
|
|
739
|
+
Retrying after expiry creates a new Update generation.
|
|
734
740
|
|
|
735
741
|
Args:
|
|
736
742
|
flow_id: The non-empty active Flow ID.
|
|
@@ -777,6 +783,10 @@ class Client:
|
|
|
777
783
|
|
|
778
784
|
The Client returns the value observed by the successful wait. The server
|
|
779
785
|
derives a stable Request ID from the condition when none is supplied.
|
|
786
|
+
Leave the maximum wait time at zero for ordinary infinite waits. A positive
|
|
787
|
+
value releases per-Flow in-flight Update capacity for rarely matching waits.
|
|
788
|
+
The budget spans transport reattachments and Continue-as-New, and is distinct
|
|
789
|
+
from the caller's lifetime. Retrying after expiry creates a new Update generation.
|
|
780
790
|
JSON, bytes, and null operands raise ``ValueError`` before transport.
|
|
781
791
|
|
|
782
792
|
Args:
|