onec-interactive-runtime-core 0.1.19__py3-none-any.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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from base64 import b64decode
|
|
4
|
+
import binascii
|
|
5
|
+
from collections.abc import Iterator, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from decimal import Decimal, InvalidOperation
|
|
9
|
+
import json
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Any
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
from onec_runtime.errors import (
|
|
15
|
+
MaterializationCycleError,
|
|
16
|
+
MaterializationKeyError,
|
|
17
|
+
MaterializationLimitError,
|
|
18
|
+
UnsupportedOnecType,
|
|
19
|
+
ValuePayloadError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _OnecSentinel:
|
|
24
|
+
__slots__ = ("_name",)
|
|
25
|
+
|
|
26
|
+
def __init__(self, name: str) -> None:
|
|
27
|
+
self._name = name
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return self._name
|
|
31
|
+
|
|
32
|
+
def __copy__(self) -> _OnecSentinel:
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __deepcopy__(self, memo: dict[int, object]) -> _OnecSentinel:
|
|
36
|
+
del memo
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ONEC_UNDEFINED = _OnecSentinel("ONEC_UNDEFINED")
|
|
41
|
+
ONEC_NULL = _OnecSentinel("ONEC_NULL")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class MaterializationOptions:
|
|
46
|
+
refs: str = "presentation"
|
|
47
|
+
max_depth: int = 32
|
|
48
|
+
max_items: int = 100_000
|
|
49
|
+
max_bytes: int = 64 * 1024 * 1024
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.refs not in {"presentation", "uuid", "both"}:
|
|
53
|
+
raise ValueError("reference mode must be presentation, uuid, or both")
|
|
54
|
+
for name in ("max_depth", "max_items", "max_bytes"):
|
|
55
|
+
value = getattr(self, name)
|
|
56
|
+
if type(value) is not int or value <= 0:
|
|
57
|
+
raise ValueError(f"{name} must be a positive integer")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class OnecReference:
|
|
62
|
+
type_name: str
|
|
63
|
+
uuid: UUID | None
|
|
64
|
+
presentation: str
|
|
65
|
+
empty: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class OnecEnumValue:
|
|
70
|
+
type_name: str
|
|
71
|
+
name: str
|
|
72
|
+
presentation: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class OnecObjectSnapshot(Mapping[str, object]):
|
|
77
|
+
type_name: str
|
|
78
|
+
attributes: Mapping[str, object]
|
|
79
|
+
tabular_sections: tuple[str, ...] = ()
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
if not self.type_name:
|
|
83
|
+
raise ValueError("1C object type name must not be empty")
|
|
84
|
+
object.__setattr__(self, "attributes", MappingProxyType(dict(self.attributes)))
|
|
85
|
+
object.__setattr__(self, "tabular_sections", tuple(self.tabular_sections))
|
|
86
|
+
|
|
87
|
+
def __getitem__(self, key: str) -> object:
|
|
88
|
+
return self.attributes[key]
|
|
89
|
+
|
|
90
|
+
def __iter__(self) -> Iterator[str]:
|
|
91
|
+
return iter(self.attributes)
|
|
92
|
+
|
|
93
|
+
def __len__(self) -> int:
|
|
94
|
+
return len(self.attributes)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class OnecTreeRow:
|
|
99
|
+
values: tuple[object, ...]
|
|
100
|
+
children: tuple[OnecTreeRow, ...] = ()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class OnecTreeSnapshot:
|
|
105
|
+
columns: tuple[str, ...]
|
|
106
|
+
rows: tuple[OnecTreeRow, ...]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def decode_value_payload(
|
|
110
|
+
payload: bytes,
|
|
111
|
+
options: MaterializationOptions | None = None,
|
|
112
|
+
) -> object:
|
|
113
|
+
selected = options or MaterializationOptions()
|
|
114
|
+
if not isinstance(payload, bytes):
|
|
115
|
+
raise TypeError("typed value payload must be bytes")
|
|
116
|
+
if len(payload) > selected.max_bytes:
|
|
117
|
+
raise MaterializationLimitError(
|
|
118
|
+
f"materialization bytes limit {selected.max_bytes} exceeded at $"
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
document = json.loads(payload.decode("utf-8"))
|
|
122
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
123
|
+
raise ValuePayloadError("typed value payload is not valid UTF-8 JSON") from error
|
|
124
|
+
if not isinstance(document, dict) or document.get("version") != 1:
|
|
125
|
+
raise ValuePayloadError("typed value payload version is invalid")
|
|
126
|
+
keys = set(document)
|
|
127
|
+
if keys == {"version", "error"}:
|
|
128
|
+
_raise_wire_error(document["error"])
|
|
129
|
+
if keys != {"version", "root"}:
|
|
130
|
+
raise ValuePayloadError("typed value payload envelope is invalid")
|
|
131
|
+
decoder = _ValueDecoder(selected)
|
|
132
|
+
return decoder.decode(document["root"], path="$", depth=0)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class _ValueDecoder:
|
|
136
|
+
__slots__ = ("options", "items")
|
|
137
|
+
|
|
138
|
+
def __init__(self, options: MaterializationOptions) -> None:
|
|
139
|
+
self.options = options
|
|
140
|
+
self.items = 0
|
|
141
|
+
|
|
142
|
+
def decode(self, node: object, *, path: str, depth: int) -> object:
|
|
143
|
+
if depth > self.options.max_depth:
|
|
144
|
+
raise MaterializationLimitError(
|
|
145
|
+
f"materialization depth limit {self.options.max_depth} exceeded at {path}"
|
|
146
|
+
)
|
|
147
|
+
if not isinstance(node, dict) or not isinstance(node.get("t"), str):
|
|
148
|
+
raise ValuePayloadError(f"typed value node is invalid at {path}")
|
|
149
|
+
tag = node["t"]
|
|
150
|
+
if tag == "undefined":
|
|
151
|
+
self._exact(node, {"t"}, path)
|
|
152
|
+
return ONEC_UNDEFINED
|
|
153
|
+
if tag == "null":
|
|
154
|
+
self._exact(node, {"t"}, path)
|
|
155
|
+
return ONEC_NULL
|
|
156
|
+
if tag == "string":
|
|
157
|
+
return self._scalar(node, str, path)
|
|
158
|
+
if tag == "boolean":
|
|
159
|
+
return self._scalar(node, bool, path)
|
|
160
|
+
if tag == "number":
|
|
161
|
+
raw = self._scalar(node, str, path)
|
|
162
|
+
try:
|
|
163
|
+
return Decimal(raw)
|
|
164
|
+
except InvalidOperation as error:
|
|
165
|
+
raise ValuePayloadError(f"number is invalid at {path}") from error
|
|
166
|
+
if tag == "datetime":
|
|
167
|
+
raw = self._scalar(node, str, path)
|
|
168
|
+
try:
|
|
169
|
+
return datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
170
|
+
except ValueError as error:
|
|
171
|
+
raise ValuePayloadError(f"datetime is invalid at {path}") from error
|
|
172
|
+
if tag == "uuid":
|
|
173
|
+
raw = self._scalar(node, str, path)
|
|
174
|
+
try:
|
|
175
|
+
return UUID(raw)
|
|
176
|
+
except ValueError as error:
|
|
177
|
+
raise ValuePayloadError(f"UUID is invalid at {path}") from error
|
|
178
|
+
if tag == "binary":
|
|
179
|
+
raw = self._scalar(node, str, path)
|
|
180
|
+
try:
|
|
181
|
+
return b64decode(raw, validate=True)
|
|
182
|
+
except (ValueError, binascii.Error) as error:
|
|
183
|
+
raise ValuePayloadError(f"binary Base64 is invalid at {path}") from error
|
|
184
|
+
if tag in {"array", "fixed_array"}:
|
|
185
|
+
values = self._sequence(node, path)
|
|
186
|
+
decoded = [
|
|
187
|
+
self.decode(value, path=f"{path}[{index}]", depth=depth + 1)
|
|
188
|
+
for index, value in enumerate(values)
|
|
189
|
+
]
|
|
190
|
+
return tuple(decoded) if tag == "fixed_array" else decoded
|
|
191
|
+
if tag == "structure":
|
|
192
|
+
return self._decode_structure(node, path=path, depth=depth)
|
|
193
|
+
if tag == "map":
|
|
194
|
+
return self._decode_map(node, path=path, depth=depth)
|
|
195
|
+
if tag == "reference":
|
|
196
|
+
return self._decode_reference(node, path)
|
|
197
|
+
if tag == "enum":
|
|
198
|
+
return self._decode_enum(node, path)
|
|
199
|
+
if tag == "object":
|
|
200
|
+
return self._decode_object(node, path=path, depth=depth)
|
|
201
|
+
if tag == "tree":
|
|
202
|
+
return self._decode_tree(node, path=path, depth=depth)
|
|
203
|
+
raise ValuePayloadError(f"unknown typed value tag {tag!r} at {path}")
|
|
204
|
+
|
|
205
|
+
def _scalar(self, node: dict[str, object], expected: type, path: str) -> Any:
|
|
206
|
+
self._exact(node, {"t", "v"}, path)
|
|
207
|
+
value = node["v"]
|
|
208
|
+
if type(value) is not expected:
|
|
209
|
+
raise ValuePayloadError(f"typed scalar value is invalid at {path}")
|
|
210
|
+
return value
|
|
211
|
+
|
|
212
|
+
def _sequence(self, node: dict[str, object], path: str) -> list[object]:
|
|
213
|
+
self._exact(node, {"t", "v"}, path)
|
|
214
|
+
values = node["v"]
|
|
215
|
+
if not isinstance(values, list):
|
|
216
|
+
raise ValuePayloadError(f"typed collection is invalid at {path}")
|
|
217
|
+
self._add_items(len(values), path)
|
|
218
|
+
return values
|
|
219
|
+
|
|
220
|
+
def _decode_structure(
|
|
221
|
+
self, node: dict[str, object], *, path: str, depth: int
|
|
222
|
+
) -> dict[str, object]:
|
|
223
|
+
pairs = self._sequence(node, path)
|
|
224
|
+
result: dict[str, object] = {}
|
|
225
|
+
normalized: set[str] = set()
|
|
226
|
+
for index, pair in enumerate(pairs):
|
|
227
|
+
if not isinstance(pair, list) or len(pair) != 2 or not isinstance(pair[0], str):
|
|
228
|
+
raise ValuePayloadError(f"structure entry is invalid at {path}[{index}]")
|
|
229
|
+
key = pair[0]
|
|
230
|
+
if key.casefold() in normalized:
|
|
231
|
+
raise ValuePayloadError(f"duplicate structure key at {path}.{key}")
|
|
232
|
+
normalized.add(key.casefold())
|
|
233
|
+
result[key] = self.decode(pair[1], path=f"{path}.{key}", depth=depth + 1)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
def _decode_map(
|
|
237
|
+
self, node: dict[str, object], *, path: str, depth: int
|
|
238
|
+
) -> dict[object, object]:
|
|
239
|
+
pairs = self._sequence(node, path)
|
|
240
|
+
result: dict[object, object] = {}
|
|
241
|
+
for index, pair in enumerate(pairs):
|
|
242
|
+
entry_path = f"{path}[{index}]"
|
|
243
|
+
if not isinstance(pair, list) or len(pair) != 2:
|
|
244
|
+
raise ValuePayloadError(f"Map entry is invalid at {entry_path}")
|
|
245
|
+
key = self.decode(pair[0], path=f"{entry_path}.key", depth=depth + 1)
|
|
246
|
+
try:
|
|
247
|
+
hash(key)
|
|
248
|
+
except TypeError as error:
|
|
249
|
+
raise MaterializationKeyError(
|
|
250
|
+
f"materialized Map key is not hashable at {entry_path}.key"
|
|
251
|
+
) from error
|
|
252
|
+
if key in result:
|
|
253
|
+
raise MaterializationKeyError(
|
|
254
|
+
f"materialized Map key collides at {entry_path}.key"
|
|
255
|
+
)
|
|
256
|
+
result[key] = self.decode(
|
|
257
|
+
pair[1], path=f"{entry_path}.value", depth=depth + 1
|
|
258
|
+
)
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
def _decode_reference(self, node: dict[str, object], path: str) -> OnecReference:
|
|
262
|
+
self._exact(node, {"t", "type", "uuid", "presentation", "empty"}, path)
|
|
263
|
+
type_name = node["type"]
|
|
264
|
+
raw_uuid = node["uuid"]
|
|
265
|
+
presentation = node["presentation"]
|
|
266
|
+
empty = node["empty"]
|
|
267
|
+
if (
|
|
268
|
+
not isinstance(type_name, str)
|
|
269
|
+
or not type_name
|
|
270
|
+
or raw_uuid is not None
|
|
271
|
+
and not isinstance(raw_uuid, str)
|
|
272
|
+
or not isinstance(presentation, str)
|
|
273
|
+
or type(empty) is not bool
|
|
274
|
+
):
|
|
275
|
+
raise ValuePayloadError(f"reference node is invalid at {path}")
|
|
276
|
+
try:
|
|
277
|
+
value_uuid = None if raw_uuid is None else UUID(raw_uuid)
|
|
278
|
+
except ValueError as error:
|
|
279
|
+
raise ValuePayloadError(f"reference UUID is invalid at {path}") from error
|
|
280
|
+
if empty and value_uuid is not None:
|
|
281
|
+
raise ValuePayloadError(f"empty reference has UUID at {path}")
|
|
282
|
+
return OnecReference(type_name, value_uuid, presentation, empty)
|
|
283
|
+
|
|
284
|
+
def _decode_enum(self, node: dict[str, object], path: str) -> OnecEnumValue:
|
|
285
|
+
self._exact(node, {"t", "type", "name", "presentation"}, path)
|
|
286
|
+
values = (node["type"], node["name"], node["presentation"])
|
|
287
|
+
if (
|
|
288
|
+
not isinstance(values[0], str)
|
|
289
|
+
or not values[0]
|
|
290
|
+
or not isinstance(values[1], str)
|
|
291
|
+
or not isinstance(values[2], str)
|
|
292
|
+
):
|
|
293
|
+
raise ValuePayloadError(f"enumeration node is invalid at {path}")
|
|
294
|
+
return OnecEnumValue(values[0], values[1], values[2])
|
|
295
|
+
|
|
296
|
+
def _decode_object(
|
|
297
|
+
self, node: dict[str, object], *, path: str, depth: int
|
|
298
|
+
) -> OnecObjectSnapshot:
|
|
299
|
+
self._exact(node, {"t", "type", "attributes", "sections"}, path)
|
|
300
|
+
type_name = node["type"]
|
|
301
|
+
attributes = node["attributes"]
|
|
302
|
+
sections = node["sections"]
|
|
303
|
+
if not isinstance(type_name, str) or not type_name:
|
|
304
|
+
raise ValuePayloadError(f"object type is invalid at {path}")
|
|
305
|
+
if not isinstance(attributes, list) or not isinstance(sections, list):
|
|
306
|
+
raise ValuePayloadError(f"object node is invalid at {path}")
|
|
307
|
+
self._add_items(len(attributes), path)
|
|
308
|
+
decoded: dict[str, object] = {}
|
|
309
|
+
normalized: set[str] = set()
|
|
310
|
+
for index, pair in enumerate(attributes):
|
|
311
|
+
if not isinstance(pair, list) or len(pair) != 2 or not isinstance(pair[0], str):
|
|
312
|
+
raise ValuePayloadError(f"object attribute is invalid at {path}[{index}]")
|
|
313
|
+
name = pair[0]
|
|
314
|
+
if name.casefold() in normalized:
|
|
315
|
+
raise ValuePayloadError(f"duplicate object attribute at {path}.{name}")
|
|
316
|
+
normalized.add(name.casefold())
|
|
317
|
+
decoded[name] = self.decode(
|
|
318
|
+
pair[1], path=f"{path}.{name}", depth=depth + 1
|
|
319
|
+
)
|
|
320
|
+
if any(not isinstance(section, str) or not section for section in sections):
|
|
321
|
+
raise ValuePayloadError(f"object tabular sections are invalid at {path}")
|
|
322
|
+
if len({section.casefold() for section in sections}) != len(sections):
|
|
323
|
+
raise ValuePayloadError(f"duplicate object tabular section at {path}")
|
|
324
|
+
return OnecObjectSnapshot(type_name, decoded, tuple(sections))
|
|
325
|
+
|
|
326
|
+
def _decode_tree(
|
|
327
|
+
self, node: dict[str, object], *, path: str, depth: int
|
|
328
|
+
) -> OnecTreeSnapshot:
|
|
329
|
+
self._exact(node, {"t", "columns", "rows"}, path)
|
|
330
|
+
columns = node["columns"]
|
|
331
|
+
rows = node["rows"]
|
|
332
|
+
if (
|
|
333
|
+
not isinstance(columns, list)
|
|
334
|
+
or any(not isinstance(column, str) or not column for column in columns)
|
|
335
|
+
or len(set(columns)) != len(columns)
|
|
336
|
+
or not isinstance(rows, list)
|
|
337
|
+
):
|
|
338
|
+
raise ValuePayloadError(f"tree node is invalid at {path}")
|
|
339
|
+
return OnecTreeSnapshot(
|
|
340
|
+
tuple(columns),
|
|
341
|
+
tuple(
|
|
342
|
+
self._decode_tree_row(
|
|
343
|
+
row,
|
|
344
|
+
columns=len(columns),
|
|
345
|
+
path=f"{path}.rows[{index}]",
|
|
346
|
+
depth=depth + 1,
|
|
347
|
+
)
|
|
348
|
+
for index, row in enumerate(rows)
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _decode_tree_row(
|
|
353
|
+
self,
|
|
354
|
+
row: object,
|
|
355
|
+
*,
|
|
356
|
+
columns: int,
|
|
357
|
+
path: str,
|
|
358
|
+
depth: int,
|
|
359
|
+
) -> OnecTreeRow:
|
|
360
|
+
if depth > self.options.max_depth:
|
|
361
|
+
raise MaterializationLimitError(
|
|
362
|
+
f"materialization depth limit {self.options.max_depth} exceeded at {path}"
|
|
363
|
+
)
|
|
364
|
+
if not isinstance(row, dict) or set(row) != {"values", "children"}:
|
|
365
|
+
raise ValuePayloadError(f"tree row is invalid at {path}")
|
|
366
|
+
values = row["values"]
|
|
367
|
+
children = row["children"]
|
|
368
|
+
if not isinstance(values, list) or len(values) != columns or not isinstance(
|
|
369
|
+
children, list
|
|
370
|
+
):
|
|
371
|
+
raise ValuePayloadError(f"tree row shape is invalid at {path}")
|
|
372
|
+
self._add_items(1, path)
|
|
373
|
+
return OnecTreeRow(
|
|
374
|
+
tuple(
|
|
375
|
+
self.decode(value, path=f"{path}.values[{index}]", depth=depth)
|
|
376
|
+
for index, value in enumerate(values)
|
|
377
|
+
),
|
|
378
|
+
tuple(
|
|
379
|
+
self._decode_tree_row(
|
|
380
|
+
child,
|
|
381
|
+
columns=columns,
|
|
382
|
+
path=f"{path}.children[{index}]",
|
|
383
|
+
depth=depth + 1,
|
|
384
|
+
)
|
|
385
|
+
for index, child in enumerate(children)
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def _add_items(self, count: int, path: str) -> None:
|
|
390
|
+
self.items += count
|
|
391
|
+
if self.items > self.options.max_items:
|
|
392
|
+
raise MaterializationLimitError(
|
|
393
|
+
f"materialization items limit {self.options.max_items} exceeded at {path}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def _exact(node: dict[str, object], expected: set[str], path: str) -> None:
|
|
398
|
+
if set(node) != expected:
|
|
399
|
+
raise ValuePayloadError(f"typed value node fields are invalid at {path}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _raise_wire_error(value: object) -> None:
|
|
403
|
+
if not isinstance(value, dict) or not isinstance(value.get("kind"), str):
|
|
404
|
+
raise ValuePayloadError("typed value error envelope is invalid")
|
|
405
|
+
kind = value["kind"]
|
|
406
|
+
path = value.get("path")
|
|
407
|
+
if not isinstance(path, str) or not path:
|
|
408
|
+
raise ValuePayloadError("typed value error path is invalid")
|
|
409
|
+
if kind == "cycle":
|
|
410
|
+
if set(value) != {"kind", "path", "first_path"} or not isinstance(
|
|
411
|
+
value.get("first_path"), str
|
|
412
|
+
):
|
|
413
|
+
raise ValuePayloadError("typed value cycle envelope is invalid")
|
|
414
|
+
raise MaterializationCycleError(
|
|
415
|
+
f"materialization cycle at {path}; first seen at {value['first_path']}"
|
|
416
|
+
)
|
|
417
|
+
if kind == "unsupported":
|
|
418
|
+
if set(value) != {"kind", "path", "type"} or not isinstance(
|
|
419
|
+
value.get("type"), str
|
|
420
|
+
):
|
|
421
|
+
raise ValuePayloadError("typed value unsupported envelope is invalid")
|
|
422
|
+
raise UnsupportedOnecType(
|
|
423
|
+
f"unsupported 1C type {value['type']} at {path}"
|
|
424
|
+
)
|
|
425
|
+
if kind in {"depth_limit", "item_limit", "byte_limit"}:
|
|
426
|
+
if set(value) != {"kind", "path", "limit"} or type(value.get("limit")) is not int:
|
|
427
|
+
raise ValuePayloadError("typed value limit envelope is invalid")
|
|
428
|
+
label = kind.removesuffix("_limit").replace("item", "items")
|
|
429
|
+
raise MaterializationLimitError(
|
|
430
|
+
f"materialization {label} limit {value['limit']} exceeded at {path}"
|
|
431
|
+
)
|
|
432
|
+
raise ValuePayloadError(f"unknown typed value error kind {kind!r}")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from base64 import b64decode
|
|
4
|
+
import binascii
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
import re
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from onec_runtime.errors import ProtocolError
|
|
11
|
+
from onec_runtime.experiment import bsl_string_literal
|
|
12
|
+
from onec_runtime.performance_profile import PhaseRecorder
|
|
13
|
+
from onec_runtime.value_materialization import (
|
|
14
|
+
MaterializationOptions,
|
|
15
|
+
decode_value_payload,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
VALUE_CONTEXT_KEY_PREFIX = "__onec_value_"
|
|
20
|
+
_HANDLE = re.compile(
|
|
21
|
+
r"Контекст\.[^\W\d]\w*(?:\.[^\W\d]\w*)*\Z",
|
|
22
|
+
re.UNICODE,
|
|
23
|
+
)
|
|
24
|
+
_CONTEXT_KEY = re.compile(r"__onec_value_[0-9a-f]{32}\Z")
|
|
25
|
+
_HASH = re.compile(r"[0-9a-f]{64}\Z")
|
|
26
|
+
_ERROR_ENVELOPE_MAX_BYTES = 4096
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_value_transfer_instruction(
|
|
30
|
+
handle: str,
|
|
31
|
+
options: MaterializationOptions,
|
|
32
|
+
context_key: str,
|
|
33
|
+
*,
|
|
34
|
+
runtime_generation: int,
|
|
35
|
+
context_generation: int,
|
|
36
|
+
) -> str:
|
|
37
|
+
validate_value_handle(handle)
|
|
38
|
+
if not _CONTEXT_KEY.fullmatch(context_key):
|
|
39
|
+
raise ProtocolError("value materialization context key is invalid")
|
|
40
|
+
if runtime_generation <= 0 or context_generation <= 0:
|
|
41
|
+
raise ProtocolError("value transfer generations must be positive")
|
|
42
|
+
return "\n".join(
|
|
43
|
+
(
|
|
44
|
+
"МатериализацияЗначения = "
|
|
45
|
+
"RuntimeValueTransferServer.СериализоватьЗначение("
|
|
46
|
+
f"{handle}, {bsl_string_literal(options.refs)}, "
|
|
47
|
+
f"{options.max_depth}, {options.max_items}, {options.max_bytes});",
|
|
48
|
+
f"Контекст.Вставить({bsl_string_literal(context_key)}, "
|
|
49
|
+
"МатериализацияЗначения.Base64);",
|
|
50
|
+
"Результат = "
|
|
51
|
+
f'Формат({runtime_generation}, "ЧГ=0; ЧДЦ=0") + "|" + '
|
|
52
|
+
f'Формат({context_generation}, "ЧГ=0; ЧДЦ=0") + "|" + '
|
|
53
|
+
'Формат(МатериализацияЗначения.Размер, "ЧГ=0; ЧДЦ=0") + "|" + '
|
|
54
|
+
'МатериализацияЗначения.Хеш + "|" + '
|
|
55
|
+
"Формат(СтрДлина(МатериализацияЗначения.Base64), "
|
|
56
|
+
'"ЧГ=0; ЧДЦ=0");',
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_value_handle(handle: str) -> str:
|
|
62
|
+
if not isinstance(handle, str) or not _HANDLE.fullmatch(handle):
|
|
63
|
+
raise ProtocolError(
|
|
64
|
+
"value handle must be one direct or dotted persistent Context path"
|
|
65
|
+
)
|
|
66
|
+
return handle
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RuntimeValueTransfer:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
instruction_executor: Callable[[str], object],
|
|
73
|
+
context_reader: Callable[[str, int], str],
|
|
74
|
+
*,
|
|
75
|
+
context_cleaner: Callable[[str], None],
|
|
76
|
+
runtime_generation: Callable[[], int],
|
|
77
|
+
context_generation: int,
|
|
78
|
+
key_factory: Callable[[], str] | None = None,
|
|
79
|
+
profiler: PhaseRecorder | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self._execute = instruction_executor
|
|
82
|
+
self._read = context_reader
|
|
83
|
+
self._clean = context_cleaner
|
|
84
|
+
self._runtime_generation = runtime_generation
|
|
85
|
+
self._context_generation = context_generation
|
|
86
|
+
self._expected_runtime_generation = runtime_generation()
|
|
87
|
+
self._key_factory = key_factory or (lambda: VALUE_CONTEXT_KEY_PREFIX + uuid4().hex)
|
|
88
|
+
self._profiler = profiler
|
|
89
|
+
|
|
90
|
+
def materialize(self, handle: str, options: MaterializationOptions) -> object:
|
|
91
|
+
payload = self.payload(handle, options)
|
|
92
|
+
return self._profile(
|
|
93
|
+
"value.decode_snapshot",
|
|
94
|
+
lambda: decode_value_payload(payload, options),
|
|
95
|
+
input_bytes=len(payload),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def payload(self, handle: str, options: MaterializationOptions) -> bytes:
|
|
99
|
+
generation = self._runtime_generation()
|
|
100
|
+
if generation != self._expected_runtime_generation:
|
|
101
|
+
raise ProtocolError("value materializer runtime generation is stale")
|
|
102
|
+
key = self._key_factory()
|
|
103
|
+
source = build_value_transfer_instruction(
|
|
104
|
+
handle,
|
|
105
|
+
options,
|
|
106
|
+
key,
|
|
107
|
+
runtime_generation=generation,
|
|
108
|
+
context_generation=self._context_generation,
|
|
109
|
+
)
|
|
110
|
+
consumed = False
|
|
111
|
+
primary_error: BaseException | None = None
|
|
112
|
+
try:
|
|
113
|
+
metadata = self._profile(
|
|
114
|
+
"value.prepare",
|
|
115
|
+
lambda: self._execute(source),
|
|
116
|
+
input_bytes=len(source.encode("utf-8")),
|
|
117
|
+
)
|
|
118
|
+
maximum_transfer_bytes = max(
|
|
119
|
+
options.max_bytes, _ERROR_ENVELOPE_MAX_BYTES
|
|
120
|
+
)
|
|
121
|
+
maximum_text_size = _base64_length(maximum_transfer_bytes)
|
|
122
|
+
content = self._profile(
|
|
123
|
+
"value.transfer_base64",
|
|
124
|
+
lambda: self._read(key, maximum_text_size),
|
|
125
|
+
output_bytes=lambda value: len(value.encode("ascii")),
|
|
126
|
+
)
|
|
127
|
+
consumed = True
|
|
128
|
+
observed = _parse_metadata(metadata)
|
|
129
|
+
if (
|
|
130
|
+
observed[0] != generation
|
|
131
|
+
or observed[1] != self._context_generation
|
|
132
|
+
or observed[2] <= 0
|
|
133
|
+
or observed[2] > maximum_transfer_bytes
|
|
134
|
+
or observed[4] <= 0
|
|
135
|
+
or observed[4] > maximum_text_size
|
|
136
|
+
or len(content) != observed[4]
|
|
137
|
+
):
|
|
138
|
+
raise ProtocolError("value materialization metadata is invalid")
|
|
139
|
+
try:
|
|
140
|
+
payload = self._profile(
|
|
141
|
+
"value.decode_base64",
|
|
142
|
+
lambda: b64decode("".join(content.split()), validate=True),
|
|
143
|
+
input_bytes=len(content.encode("ascii")),
|
|
144
|
+
output_bytes=len,
|
|
145
|
+
)
|
|
146
|
+
except (ValueError, binascii.Error) as error:
|
|
147
|
+
raise ProtocolError("value materialization Base64 payload is invalid") from error
|
|
148
|
+
if len(payload) != observed[2] or sha256(payload).hexdigest() != observed[3]:
|
|
149
|
+
raise ProtocolError("value materialization payload integrity check failed")
|
|
150
|
+
return payload
|
|
151
|
+
except BaseException as error:
|
|
152
|
+
primary_error = error
|
|
153
|
+
raise
|
|
154
|
+
finally:
|
|
155
|
+
if not consumed:
|
|
156
|
+
try:
|
|
157
|
+
self._clean(key)
|
|
158
|
+
except BaseException as cleanup_error:
|
|
159
|
+
if primary_error is not None:
|
|
160
|
+
primary_error.add_note(
|
|
161
|
+
"value materialization cleanup failed: "
|
|
162
|
+
f"{type(cleanup_error).__name__}: {cleanup_error}"
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
def _profile(self, phase: str, operation, **metadata): # type: ignore[no-untyped-def]
|
|
168
|
+
if self._profiler is None:
|
|
169
|
+
return operation()
|
|
170
|
+
return self._profiler.measure(phase, operation, **metadata)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_metadata(value: object) -> tuple[int, int, int, str, int]:
|
|
174
|
+
if not isinstance(value, str):
|
|
175
|
+
raise ProtocolError("value materialization metadata is not a string")
|
|
176
|
+
fields = value.split("|")
|
|
177
|
+
if len(fields) != 5:
|
|
178
|
+
raise ProtocolError("value materialization metadata field count is invalid")
|
|
179
|
+
try:
|
|
180
|
+
runtime_generation = int(fields[0])
|
|
181
|
+
context_generation = int(fields[1])
|
|
182
|
+
byte_count = int(fields[2])
|
|
183
|
+
base64_count = int(fields[4])
|
|
184
|
+
except ValueError as error:
|
|
185
|
+
raise ProtocolError("value materialization metadata number is invalid") from error
|
|
186
|
+
payload_hash = fields[3]
|
|
187
|
+
if not _HASH.fullmatch(payload_hash):
|
|
188
|
+
raise ProtocolError("value materialization metadata hash is invalid")
|
|
189
|
+
return (
|
|
190
|
+
runtime_generation,
|
|
191
|
+
context_generation,
|
|
192
|
+
byte_count,
|
|
193
|
+
payload_hash,
|
|
194
|
+
base64_count,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _base64_length(byte_count: int) -> int:
|
|
199
|
+
return ((byte_count + 2) // 3) * 4
|