dex-python-sdk 0.0.2__cp311-abi3-win_amd64.whl
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/__init__.py +146 -0
- dex/_grpc_errors.py +73 -0
- dex/_invocation_context.py +269 -0
- dex/_native.pyd +0 -0
- dex/_native.pyi +20 -0
- dex/_utils.py +18 -0
- dex/_value_hydrator.py +223 -0
- dex/_value_mapper.py +174 -0
- dex/_worker_dispatcher.py +451 -0
- dex/_worker_service.py +60 -0
- dex/attribute.py +87 -0
- dex/blob_cache.py +73 -0
- dex/channel.py +165 -0
- dex/client.py +715 -0
- dex/client_options.py +19 -0
- dex/codec.py +286 -0
- dex/command_request.py +120 -0
- dex/command_results.py +107 -0
- dex/communication.py +136 -0
- dex/communication_schema.py +54 -0
- dex/condition.py +74 -0
- dex/context.py +86 -0
- dex/data_attributes.py +70 -0
- dex/dexpb/__init__.py +1 -0
- dex/dexpb/dex_pb2.py +381 -0
- dex/dexpb/dex_pb2.pyi +1734 -0
- dex/dexpb/dex_pb2_grpc.py +1298 -0
- dex/errors.py +109 -0
- dex/flow.py +456 -0
- dex/flow_config.py +29 -0
- dex/flow_info.py +51 -0
- dex/flow_options.py +122 -0
- dex/object_encoder.py +799 -0
- dex/persistence.py +89 -0
- dex/persistence_options.py +12 -0
- dex/persistence_schema.py +51 -0
- dex/py.typed +1 -0
- dex/registry.py +204 -0
- dex/reset_workflow_type_and_options.py +67 -0
- dex/rpc.py +93 -0
- dex/runtime_errors.py +81 -0
- dex/search_attributes.py +184 -0
- dex/state_decision.py +153 -0
- dex/state_execution_locals.py +66 -0
- dex/state_movement.py +115 -0
- dex/state_schema.py +48 -0
- dex/step.py +194 -0
- dex/step_execution.py +42 -0
- dex/stop_workflow_options.py +18 -0
- dex/tests/__init__.py +80 -0
- dex/tests/dex-service-env/.env +7 -0
- dex/tests/dex-service-env/docker-compose-init.sh +44 -0
- dex/tests/dex-service-env/docker-compose.yml +97 -0
- dex/tests/dex-service-env/dynamicconfig/README.md +39 -0
- dex/tests/dex-service-env/dynamicconfig/development-sql.yaml +9 -0
- dex/tests/dex-service-env/dynamicconfig/docker.yaml +2 -0
- dex/tests/test_abnormal_exit_workflow.py +43 -0
- dex/tests/test_basic_workflow.py +70 -0
- dex/tests/test_conditional_complete.py +50 -0
- dex/tests/test_describe_workflow.py +40 -0
- dex/tests/test_empty_data_decodes_properly.py +74 -0
- dex/tests/test_internal_channel.py +28 -0
- dex/tests/test_internal_channel_with_no_prefix_channel.py +41 -0
- dex/tests/test_persistence_data_attributes.py +62 -0
- dex/tests/test_persistence_search_attributes.py +127 -0
- dex/tests/test_persistence_state_execution_locals.py +38 -0
- dex/tests/test_rpc.py +64 -0
- dex/tests/test_rpc_with_memo.py +195 -0
- dex/tests/test_rpc_with_memo_duplicate_java_tests.py +117 -0
- dex/tests/test_signal.py +51 -0
- dex/tests/test_skip_wait_until.py +76 -0
- dex/tests/test_state_failure_recovery.py +28 -0
- dex/tests/test_timer.py +35 -0
- dex/tests/test_wait_for_state_execution_completion.py +53 -0
- dex/tests/test_workflow_errors.py +87 -0
- dex/tests/test_workflow_state_options.py +118 -0
- dex/tests/test_workflow_state_options_override.py +44 -0
- dex/tests/worker_server.py +64 -0
- dex/tests/workflows/abnormal_exit_workflow.py +42 -0
- dex/tests/workflows/basic_workflow.py +62 -0
- dex/tests/workflows/conditional_complete_workflow.py +95 -0
- dex/tests/workflows/describe_workflow.py +46 -0
- dex/tests/workflows/empty_data_workflow.py +45 -0
- dex/tests/workflows/internal_channel_workflow.py +129 -0
- dex/tests/workflows/internal_channel_workflow_with_no_prefix_channel.py +100 -0
- dex/tests/workflows/java_duplicate_rpc_memo_workflow.py +276 -0
- dex/tests/workflows/persistence_data_attributes_workflow.py +98 -0
- dex/tests/workflows/persistence_search_attributes_workflow.py +159 -0
- dex/tests/workflows/persistence_state_execution_local_workflow.py +63 -0
- dex/tests/workflows/recovery_workflow.py +82 -0
- dex/tests/workflows/rpc_memo_workflow.py +231 -0
- dex/tests/workflows/rpc_workflow.py +117 -0
- dex/tests/workflows/state_options_override_workflow.py +93 -0
- dex/tests/workflows/state_options_workflow.py +84 -0
- dex/tests/workflows/timer_workflow.py +46 -0
- dex/tests/workflows/wait_for_state_with_state_execution_id_workflow.py +70 -0
- dex/tests/workflows/wait_for_state_with_wait_for_key_workflow.py +71 -0
- dex/tests/workflows/wait_internal_channel_workflow.py +47 -0
- dex/tests/workflows/wait_signal_workflow.py +147 -0
- dex/timer.py +21 -0
- dex/type_store.py +99 -0
- dex/unregistered_client.py +585 -0
- dex/utils/__init__.py +3 -0
- dex/utils/dex_typing.py +25 -0
- dex/utils/persistence_utils.py +32 -0
- dex/wait.py +49 -0
- dex/worker.py +121 -0
- dex/worker_options.py +22 -0
- dex/worker_service.py +432 -0
- dex/workflow.py +79 -0
- dex/workflow_context.py +44 -0
- dex/workflow_info.py +16 -0
- dex/workflow_options.py +74 -0
- dex/workflow_state.py +123 -0
- dex/workflow_state_options.py +154 -0
- dex_python_sdk-0.0.2.dist-info/METADATA +202 -0
- dex_python_sdk-0.0.2.dist-info/RECORD +121 -0
- dex_python_sdk-0.0.2.dist-info/WHEEL +4 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LEGACY_NOTICES.md +61 -0
- dex_python_sdk-0.0.2.dist-info/licenses/LICENSE +192 -0
- dex_python_sdk-0.0.2.dist-info/sboms/dex-blob-cache-python.cyclonedx.json +2406 -0
dex/_value_hydrator.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Super Durable Source 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-Super-Durable-1.0
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from dex.blob_cache import BlobCache
|
|
15
|
+
from dex.dexpb import dex_pb2 as pb
|
|
16
|
+
from dex.dexpb import dex_pb2_grpc
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _PendingBlob:
|
|
23
|
+
blob_id: str
|
|
24
|
+
is_object: bool
|
|
25
|
+
request: pb.Value
|
|
26
|
+
indexes: list[int] = field(default_factory=list)
|
|
27
|
+
hydrated: pb.Value | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValueHydrator:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
service: dex_pb2_grpc.FlowServiceStub,
|
|
34
|
+
cache: BlobCache,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._service = service
|
|
37
|
+
self._cache = cache
|
|
38
|
+
|
|
39
|
+
def hydrate(self, value: pb.Value) -> pb.Value:
|
|
40
|
+
return self.hydrate_all([value])[0]
|
|
41
|
+
|
|
42
|
+
def hydrate_all(self, values: list[pb.Value]) -> list[pb.Value]:
|
|
43
|
+
hydrated = list(values)
|
|
44
|
+
pending: dict[tuple[str, bool], _PendingBlob] = {}
|
|
45
|
+
for index, value in enumerate(values):
|
|
46
|
+
key = self._blob_key(value)
|
|
47
|
+
if key is None:
|
|
48
|
+
self._validate_concrete(value)
|
|
49
|
+
continue
|
|
50
|
+
blob = pending.get(key)
|
|
51
|
+
if blob is None:
|
|
52
|
+
blob = _PendingBlob(key[0], key[1], value)
|
|
53
|
+
pending[key] = blob
|
|
54
|
+
blob.indexes.append(index)
|
|
55
|
+
|
|
56
|
+
misses: list[_PendingBlob] = []
|
|
57
|
+
for blob in pending.values():
|
|
58
|
+
concrete = self._read_cache(blob)
|
|
59
|
+
if concrete is None:
|
|
60
|
+
misses.append(blob)
|
|
61
|
+
else:
|
|
62
|
+
blob.hydrated = concrete
|
|
63
|
+
self._load_misses(misses)
|
|
64
|
+
|
|
65
|
+
for blob in pending.values():
|
|
66
|
+
if blob.hydrated is None:
|
|
67
|
+
raise RuntimeError(f"blob was not hydrated: {blob.blob_id}")
|
|
68
|
+
for index in blob.indexes:
|
|
69
|
+
hydrated[index] = blob.hydrated
|
|
70
|
+
return hydrated
|
|
71
|
+
|
|
72
|
+
def wait_for_request(
|
|
73
|
+
self,
|
|
74
|
+
request: pb.InvokeWaitForMethodRequest,
|
|
75
|
+
) -> pb.InvokeWaitForMethodRequest:
|
|
76
|
+
result = pb.InvokeWaitForMethodRequest()
|
|
77
|
+
result.CopyFrom(request)
|
|
78
|
+
values = [request.step_input, *(entry.value for entry in request.attributes)]
|
|
79
|
+
hydrated = self.hydrate_all(values)
|
|
80
|
+
result.step_input.CopyFrom(hydrated[0])
|
|
81
|
+
for entry, value in zip(result.attributes, hydrated[1:]):
|
|
82
|
+
entry.value.CopyFrom(value)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def execute_request(
|
|
86
|
+
self,
|
|
87
|
+
request: pb.InvokeExecuteMethodRequest,
|
|
88
|
+
) -> pb.InvokeExecuteMethodRequest:
|
|
89
|
+
result = pb.InvokeExecuteMethodRequest()
|
|
90
|
+
result.CopyFrom(request)
|
|
91
|
+
values = [request.step_input]
|
|
92
|
+
values.extend(entry.value for entry in request.attributes)
|
|
93
|
+
values.extend(entry.value for entry in request.step_exe_locals)
|
|
94
|
+
for channel_result in request.condition_results.channel_results:
|
|
95
|
+
values.extend(channel_result.values)
|
|
96
|
+
hydrated = iter(self.hydrate_all(values))
|
|
97
|
+
result.step_input.CopyFrom(next(hydrated))
|
|
98
|
+
for entry in result.attributes:
|
|
99
|
+
entry.value.CopyFrom(next(hydrated))
|
|
100
|
+
for entry in result.step_exe_locals:
|
|
101
|
+
entry.value.CopyFrom(next(hydrated))
|
|
102
|
+
for channel_result in result.condition_results.channel_results:
|
|
103
|
+
for value in channel_result.values:
|
|
104
|
+
value.CopyFrom(next(hydrated))
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
def rpc_request(
|
|
108
|
+
self,
|
|
109
|
+
request: pb.InvokeWorkerRPCRequest,
|
|
110
|
+
) -> pb.InvokeWorkerRPCRequest:
|
|
111
|
+
result = pb.InvokeWorkerRPCRequest()
|
|
112
|
+
result.CopyFrom(request)
|
|
113
|
+
values = [request.input, *(entry.value for entry in request.attributes)]
|
|
114
|
+
hydrated = self.hydrate_all(values)
|
|
115
|
+
result.input.CopyFrom(hydrated[0])
|
|
116
|
+
for entry, value in zip(result.attributes, hydrated[1:]):
|
|
117
|
+
entry.value.CopyFrom(value)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def step_outputs(
|
|
121
|
+
self,
|
|
122
|
+
outputs: list[pb.StepCompletionOutput],
|
|
123
|
+
) -> list[pb.StepCompletionOutput]:
|
|
124
|
+
values = [output.completed_step_output for output in outputs]
|
|
125
|
+
hydrated = self.hydrate_all(values)
|
|
126
|
+
results: list[pb.StepCompletionOutput] = []
|
|
127
|
+
for output, value in zip(outputs, hydrated):
|
|
128
|
+
result = pb.StepCompletionOutput()
|
|
129
|
+
result.CopyFrom(output)
|
|
130
|
+
result.completed_step_output.CopyFrom(value)
|
|
131
|
+
results.append(result)
|
|
132
|
+
return results
|
|
133
|
+
|
|
134
|
+
def _load_misses(self, misses: list[_PendingBlob]) -> None:
|
|
135
|
+
if not misses:
|
|
136
|
+
return
|
|
137
|
+
response = self._service.LoadBlobs(
|
|
138
|
+
pb.LoadBlobsRequest(values=[miss.request for miss in misses])
|
|
139
|
+
)
|
|
140
|
+
for miss in misses:
|
|
141
|
+
concrete = response.values.get(miss.blob_id)
|
|
142
|
+
if concrete is None:
|
|
143
|
+
raise RuntimeError(f"LoadBlobs omitted blob {miss.blob_id}")
|
|
144
|
+
self._validate_hydrated(miss, concrete)
|
|
145
|
+
miss.hydrated = concrete
|
|
146
|
+
self._write_cache(miss, concrete)
|
|
147
|
+
|
|
148
|
+
def _read_cache(self, blob: _PendingBlob) -> pb.Value | None:
|
|
149
|
+
try:
|
|
150
|
+
payload = self._cache.get(blob.blob_id)
|
|
151
|
+
if payload is None:
|
|
152
|
+
return None
|
|
153
|
+
if blob.is_object:
|
|
154
|
+
concrete = pb.Value(obj_value=pb.EncodedObject.FromString(payload))
|
|
155
|
+
else:
|
|
156
|
+
concrete = pb.Value(string_value=payload.decode("utf-8"))
|
|
157
|
+
self._validate_hydrated(blob, concrete)
|
|
158
|
+
return concrete
|
|
159
|
+
except Exception:
|
|
160
|
+
_LOGGER.warning("cannot read cached blob %s", blob.blob_id, exc_info=True)
|
|
161
|
+
try:
|
|
162
|
+
self._cache.delete(blob.blob_id)
|
|
163
|
+
except Exception:
|
|
164
|
+
_LOGGER.warning(
|
|
165
|
+
"cannot delete cached blob %s",
|
|
166
|
+
blob.blob_id,
|
|
167
|
+
exc_info=True,
|
|
168
|
+
)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def _write_cache(self, blob: _PendingBlob, concrete: pb.Value) -> None:
|
|
172
|
+
try:
|
|
173
|
+
payload = (
|
|
174
|
+
concrete.obj_value.SerializeToString()
|
|
175
|
+
if blob.is_object
|
|
176
|
+
else concrete.string_value.encode("utf-8")
|
|
177
|
+
)
|
|
178
|
+
self._cache.put(blob.blob_id, payload)
|
|
179
|
+
except Exception:
|
|
180
|
+
_LOGGER.warning("cannot cache blob %s", blob.blob_id, exc_info=True)
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _blob_key(value: pb.Value) -> tuple[str, bool] | None:
|
|
184
|
+
kind = value.WhichOneof("kind")
|
|
185
|
+
if kind == "internal_blob_id_for_string_value":
|
|
186
|
+
blob_id = value.internal_blob_id_for_string_value
|
|
187
|
+
if not blob_id:
|
|
188
|
+
raise ValueError("blob ID is required")
|
|
189
|
+
return blob_id, False
|
|
190
|
+
if kind == "internal_blob_id_for_obj_value":
|
|
191
|
+
blob_id = value.internal_blob_id_for_obj_value
|
|
192
|
+
if not blob_id:
|
|
193
|
+
raise ValueError("blob ID is required")
|
|
194
|
+
return blob_id, True
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def _validate_hydrated(blob: _PendingBlob, value: pb.Value) -> None:
|
|
199
|
+
expected = "obj_value" if blob.is_object else "string_value"
|
|
200
|
+
if value.WhichOneof("kind") != expected:
|
|
201
|
+
raise RuntimeError(
|
|
202
|
+
f"blob {blob.blob_id} hydrated to {value.WhichOneof('kind')}"
|
|
203
|
+
)
|
|
204
|
+
ValueHydrator._validate_concrete(value)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _validate_concrete(value: pb.Value) -> None:
|
|
208
|
+
kind = value.WhichOneof("kind")
|
|
209
|
+
if kind in ("string_value", "int_value", "bool_value", "null_value"):
|
|
210
|
+
return
|
|
211
|
+
if kind == "double_value":
|
|
212
|
+
import math
|
|
213
|
+
|
|
214
|
+
if not math.isfinite(value.double_value):
|
|
215
|
+
raise ValueError("non-finite numbers are unsupported")
|
|
216
|
+
return
|
|
217
|
+
if kind == "obj_value":
|
|
218
|
+
if value.obj_value.encoding not in ("json", "rawbytes"):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"unsupported object encoding {value.obj_value.encoding}"
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
raise ValueError("Value has no concrete kind")
|
dex/_value_mapper.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Super Durable Source 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-Super-Durable-1.0
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict, is_dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
from google.protobuf import struct_pb2
|
|
18
|
+
|
|
19
|
+
from dex.attribute import AttributeIndex, IndexType
|
|
20
|
+
from dex.codec import Codec, CodecRegistry, Value, WireKind
|
|
21
|
+
from dex.dexpb import dex_pb2 as pb
|
|
22
|
+
|
|
23
|
+
_JSON_ENCODING = "json"
|
|
24
|
+
_RAW_BYTES_ENCODING = "rawbytes"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ValueMapper:
|
|
28
|
+
def __init__(self, codecs: CodecRegistry) -> None:
|
|
29
|
+
self._codecs = codecs
|
|
30
|
+
|
|
31
|
+
def codec(self, value_type: object) -> Codec[Any]:
|
|
32
|
+
return self._codecs.resolve(value_type)
|
|
33
|
+
|
|
34
|
+
def encode(self, value: Any, codec: Codec[Any]) -> pb.Value:
|
|
35
|
+
if value is None:
|
|
36
|
+
return pb.Value(
|
|
37
|
+
obj_value=pb.EncodedObject(
|
|
38
|
+
encoding=_JSON_ENCODING,
|
|
39
|
+
payload=b"null",
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
logical = codec.encode(value)
|
|
43
|
+
if logical.kind is WireKind.STRING:
|
|
44
|
+
return pb.Value(string_value=self._require(logical.data, str))
|
|
45
|
+
if logical.kind is WireKind.BOOL:
|
|
46
|
+
return pb.Value(bool_value=self._require_exact(logical.data, bool))
|
|
47
|
+
if logical.kind is WireKind.INT64:
|
|
48
|
+
return pb.Value(int_value=self._require_exact(logical.data, int))
|
|
49
|
+
if logical.kind is WireKind.DOUBLE:
|
|
50
|
+
return pb.Value(double_value=self._require_exact(logical.data, float))
|
|
51
|
+
if logical.kind is WireKind.BYTES:
|
|
52
|
+
return pb.Value(
|
|
53
|
+
obj_value=pb.EncodedObject(
|
|
54
|
+
encoding=_RAW_BYTES_ENCODING,
|
|
55
|
+
payload=self._require(logical.data, bytes),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if logical.kind is WireKind.JSON:
|
|
59
|
+
payload = self._require(logical.data, str).encode("utf-8")
|
|
60
|
+
return pb.Value(
|
|
61
|
+
obj_value=pb.EncodedObject(
|
|
62
|
+
encoding=_JSON_ENCODING,
|
|
63
|
+
payload=payload,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
raise TypeError(f"unsupported wire kind {logical.kind}")
|
|
67
|
+
|
|
68
|
+
def encode_dynamic(self, value: Any) -> pb.Value:
|
|
69
|
+
if value is None:
|
|
70
|
+
return self.encode(value, self._codecs.resolve(type(None)))
|
|
71
|
+
value_type = type(value)
|
|
72
|
+
try:
|
|
73
|
+
codec = self._codecs.resolve(value_type)
|
|
74
|
+
except TypeError:
|
|
75
|
+
if isinstance(value, (list, tuple, dict)):
|
|
76
|
+
return pb.Value(
|
|
77
|
+
obj_value=pb.EncodedObject(
|
|
78
|
+
encoding=_JSON_ENCODING,
|
|
79
|
+
payload=self._encode_json_collection(value),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
raise
|
|
83
|
+
return self.encode(value, codec)
|
|
84
|
+
|
|
85
|
+
def decode(self, value: pb.Value, codec: Codec[Any]) -> Any:
|
|
86
|
+
kind = value.WhichOneof("kind")
|
|
87
|
+
expected = codec.wire_kind
|
|
88
|
+
if kind == "string_value" and expected is WireKind.STRING:
|
|
89
|
+
return codec.decode(Value(WireKind.STRING, value.string_value))
|
|
90
|
+
if kind == "bool_value" and expected is WireKind.BOOL:
|
|
91
|
+
return codec.decode(Value(WireKind.BOOL, value.bool_value))
|
|
92
|
+
if kind == "int_value" and expected is WireKind.INT64:
|
|
93
|
+
return codec.decode(Value(WireKind.INT64, value.int_value))
|
|
94
|
+
if kind == "double_value" and expected is WireKind.DOUBLE:
|
|
95
|
+
return codec.decode(Value(WireKind.DOUBLE, value.double_value))
|
|
96
|
+
if kind == "obj_value" and expected is WireKind.BYTES:
|
|
97
|
+
self._require_encoding(value.obj_value, _RAW_BYTES_ENCODING)
|
|
98
|
+
return codec.decode(Value(WireKind.BYTES, value.obj_value.payload))
|
|
99
|
+
if kind == "obj_value" and expected is WireKind.JSON:
|
|
100
|
+
self._require_encoding(value.obj_value, _JSON_ENCODING)
|
|
101
|
+
payload = value.obj_value.payload.decode("utf-8")
|
|
102
|
+
return codec.decode(Value(WireKind.JSON, payload))
|
|
103
|
+
if kind == "obj_value" and value.obj_value.encoding == _JSON_ENCODING:
|
|
104
|
+
if value.obj_value.payload == b"null":
|
|
105
|
+
return None
|
|
106
|
+
if kind in (
|
|
107
|
+
"internal_blob_id_for_string_value",
|
|
108
|
+
"internal_blob_id_for_obj_value",
|
|
109
|
+
):
|
|
110
|
+
raise ValueError("blob-backed Value was not hydrated")
|
|
111
|
+
if kind == "null_value":
|
|
112
|
+
raise ValueError("attribute deletion marker cannot be decoded")
|
|
113
|
+
raise TypeError(f"cannot decode {kind or 'empty Value'} as {codec.type_name}")
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def deletion() -> pb.Value:
|
|
117
|
+
return pb.Value(null_value=cast(struct_pb2.NullValue, struct_pb2.NULL_VALUE))
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def index_config(
|
|
121
|
+
index: AttributeIndex | None,
|
|
122
|
+
dynamic: bool,
|
|
123
|
+
) -> pb.IndexConfig | None:
|
|
124
|
+
if index is None:
|
|
125
|
+
return None
|
|
126
|
+
mapped_types = {
|
|
127
|
+
IndexType.KEYWORD: pb.INDEX_TYPE_KEYWORD,
|
|
128
|
+
IndexType.FULL_TEXT: pb.INDEX_TYPE_TEXT,
|
|
129
|
+
IndexType.KEYWORD_ARRAY: pb.INDEX_TYPE_KEYWORD_ARRAY,
|
|
130
|
+
IndexType.INT: pb.INDEX_TYPE_INT,
|
|
131
|
+
IndexType.DOUBLE: pb.INDEX_TYPE_DOUBLE,
|
|
132
|
+
IndexType.BOOL: pb.INDEX_TYPE_BOOL,
|
|
133
|
+
IndexType.DATETIME: pb.INDEX_TYPE_DATETIME,
|
|
134
|
+
}
|
|
135
|
+
config = pb.IndexConfig(enable=True, type=mapped_types[index.type])
|
|
136
|
+
if index.index_key or dynamic:
|
|
137
|
+
config.index_key = index.index_key
|
|
138
|
+
return config
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _require(value: object, expected: type[Any]) -> Any:
|
|
142
|
+
if not isinstance(value, expected):
|
|
143
|
+
raise TypeError(f"expected {expected.__name__}")
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _require_exact(value: object, expected: type[Any]) -> Any:
|
|
148
|
+
if type(value) is not expected:
|
|
149
|
+
raise TypeError(f"expected {expected.__name__}")
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _require_encoding(value: pb.EncodedObject, expected: str) -> None:
|
|
154
|
+
if value.encoding != expected:
|
|
155
|
+
raise TypeError(f"expected {expected} encoding, got {value.encoding}")
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _encode_json_collection(value: object) -> bytes:
|
|
159
|
+
def default(item: object) -> object:
|
|
160
|
+
if is_dataclass(item) and not isinstance(item, type):
|
|
161
|
+
return asdict(item)
|
|
162
|
+
if isinstance(item, datetime):
|
|
163
|
+
return item.isoformat()
|
|
164
|
+
if isinstance(item, Enum):
|
|
165
|
+
return item.value
|
|
166
|
+
raise TypeError(f"cannot encode {type(item).__name__} as JSON")
|
|
167
|
+
|
|
168
|
+
return json.dumps(
|
|
169
|
+
value,
|
|
170
|
+
default=default,
|
|
171
|
+
allow_nan=False,
|
|
172
|
+
separators=(",", ":"),
|
|
173
|
+
sort_keys=True,
|
|
174
|
+
).encode("utf-8")
|