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.
Files changed (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. 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