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,471 @@
1
+ from __future__ import annotations
2
+
3
+ from base64 import b64decode
4
+ import binascii
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from decimal import Decimal, InvalidOperation
8
+ from enum import Enum
9
+ from hashlib import sha256
10
+ import json
11
+ import re
12
+ from typing import Mapping, Protocol, cast
13
+ from uuid import UUID
14
+
15
+ import pandas as pd
16
+
17
+
18
+ class TableMaterializationError(ValueError):
19
+ """The server table snapshot is malformed or violates the public contract."""
20
+
21
+
22
+ class TableCleanupError(TableMaterializationError):
23
+ def __init__(
24
+ self,
25
+ primary: BaseException | None,
26
+ cleanup: BaseException,
27
+ ) -> None:
28
+ self.primary = primary
29
+ self.cleanup = cleanup
30
+ message = f"table transfer cleanup failed: {cleanup}"
31
+ if primary is not None:
32
+ message = f"{primary}; additionally, {message}"
33
+ super().__init__(message)
34
+
35
+
36
+ class ReferenceMode(str, Enum):
37
+ PRESENTATION = "presentation"
38
+ UUID = "uuid"
39
+ BOTH = "both"
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class ReferencePolicy:
44
+ refs: str | ReferenceMode = ReferenceMode.PRESENTATION
45
+ ref_columns: Mapping[str, str | ReferenceMode] | None = None
46
+ uuid_suffix: str = "__uuid"
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class TableColumn:
51
+ name: str
52
+ ordinal: int
53
+ kinds: tuple[str, ...]
54
+ reference: bool
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class TableSchema:
59
+ version: int
60
+ columns: tuple[TableColumn, ...]
61
+
62
+
63
+ @dataclass(frozen=True, slots=True)
64
+ class TableTransferManifest:
65
+ token: str
66
+ runtime_generation: int
67
+ context_generation: int
68
+ byte_count: int
69
+ schema_byte_count: int
70
+ chunk_count: int
71
+ payload_sha256: str
72
+ schema_sha256: str
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class TableTransferChunk:
77
+ sequence: int
78
+ content_base64: str
79
+
80
+
81
+ class TableTransferBackend(Protocol):
82
+ def start(self, handle: str, chunk_size: int) -> str: ...
83
+
84
+ def manifest(self, token: str) -> TableTransferManifest: ...
85
+
86
+ def chunk(self, token: str, sequence: int) -> TableTransferChunk: ...
87
+
88
+ def close(self, token: str) -> None: ...
89
+
90
+
91
+ class TableMaterializer:
92
+ def __init__(
93
+ self,
94
+ backend: TableTransferBackend,
95
+ *,
96
+ runtime_generation: int,
97
+ context_generation: int,
98
+ chunk_size: int = 65_536,
99
+ ) -> None:
100
+ if runtime_generation <= 0 or context_generation <= 0:
101
+ raise ValueError("materializer generations must be positive")
102
+ if chunk_size <= 0:
103
+ raise ValueError("chunk size must be positive")
104
+ self.backend = backend
105
+ self.runtime_generation = runtime_generation
106
+ self.context_generation = context_generation
107
+ self.chunk_size = chunk_size
108
+
109
+ def to_df(self, handle: str, policy: ReferencePolicy) -> pd.DataFrame:
110
+ if not handle:
111
+ raise TableMaterializationError("table handle must not be empty")
112
+ token: str | None = None
113
+ primary: BaseException | None = None
114
+ try:
115
+ token = self.backend.start(handle, self.chunk_size)
116
+ if not token:
117
+ raise TableMaterializationError("table transfer token is empty")
118
+ manifest = self.backend.manifest(token)
119
+ _validate_manifest(
120
+ manifest,
121
+ token=token,
122
+ runtime_generation=self.runtime_generation,
123
+ context_generation=self.context_generation,
124
+ )
125
+ parts: list[bytes] = []
126
+ for sequence in range(manifest.chunk_count):
127
+ chunk = self.backend.chunk(token, sequence)
128
+ if chunk.sequence != sequence:
129
+ raise TableMaterializationError(
130
+ f"table chunk sequence mismatch: expected {sequence}, "
131
+ f"got {chunk.sequence}"
132
+ )
133
+ try:
134
+ content = b64decode(chunk.content_base64, validate=True)
135
+ except (ValueError, binascii.Error) as error:
136
+ raise TableMaterializationError(
137
+ f"table chunk {sequence} is not valid Base64"
138
+ ) from error
139
+ parts.append(content)
140
+ payload = b"".join(parts)
141
+ if len(payload) != manifest.byte_count:
142
+ raise TableMaterializationError(
143
+ "table payload byte count does not match manifest"
144
+ )
145
+ if sha256(payload).hexdigest() != manifest.payload_sha256:
146
+ raise TableMaterializationError(
147
+ "table payload SHA-256 does not match manifest"
148
+ )
149
+ if (
150
+ type(manifest.schema_byte_count) is not int
151
+ or manifest.schema_byte_count <= 0
152
+ or manifest.schema_byte_count >= len(payload)
153
+ ):
154
+ raise TableMaterializationError(
155
+ "table manifest schema byte count is invalid"
156
+ )
157
+ schema_record = payload[: manifest.schema_byte_count]
158
+ delimiter = payload[manifest.schema_byte_count :]
159
+ if not delimiter.startswith((b"\n", b"\r\n")):
160
+ raise TableMaterializationError(
161
+ "table schema record is not followed by a JSONL delimiter"
162
+ )
163
+ observed_schema_sha256 = sha256(schema_record).hexdigest()
164
+ if observed_schema_sha256 != manifest.schema_sha256:
165
+ raise TableMaterializationError(
166
+ "table schema SHA-256 does not match manifest: "
167
+ f"expected={manifest.schema_sha256}, "
168
+ f"observed={observed_schema_sha256}"
169
+ )
170
+ return decode_table_payload(payload, policy)
171
+ except BaseException as error:
172
+ primary = error
173
+ raise
174
+ finally:
175
+ if token is not None:
176
+ try:
177
+ self.backend.close(token)
178
+ except BaseException as cleanup:
179
+ raise TableCleanupError(primary, cleanup) from cleanup
180
+
181
+
182
+ _SCALAR_TAGS = frozenset(
183
+ {"string", "boolean", "number", "datetime", "uuid", "undefined", "null"}
184
+ )
185
+
186
+
187
+ def _validate_manifest(
188
+ manifest: TableTransferManifest,
189
+ *,
190
+ token: str,
191
+ runtime_generation: int,
192
+ context_generation: int,
193
+ ) -> None:
194
+ if manifest.token != token:
195
+ raise TableMaterializationError("table manifest token mismatch")
196
+ if manifest.runtime_generation != runtime_generation:
197
+ raise TableMaterializationError("table manifest runtime generation mismatch")
198
+ if manifest.context_generation != context_generation:
199
+ raise TableMaterializationError("table manifest context generation mismatch")
200
+ if type(manifest.byte_count) is not int or manifest.byte_count <= 0:
201
+ raise TableMaterializationError("table manifest byte count is invalid")
202
+ if type(manifest.chunk_count) is not int or manifest.chunk_count <= 0:
203
+ raise TableMaterializationError("table manifest chunk count is invalid")
204
+ for label, value in (
205
+ ("payload", manifest.payload_sha256),
206
+ ("schema", manifest.schema_sha256),
207
+ ):
208
+ if not re.fullmatch(r"[0-9a-f]{64}", value):
209
+ raise TableMaterializationError(
210
+ f"table manifest {label} SHA-256 is invalid"
211
+ )
212
+
213
+
214
+ def decode_table_payload(
215
+ payload: bytes,
216
+ policy: ReferencePolicy | None = None,
217
+ ) -> pd.DataFrame:
218
+ selected_policy = policy or ReferencePolicy()
219
+ records = _read_records(payload)
220
+ schema = _read_schema(records[0])
221
+ modes = _validate_policy(schema, selected_policy)
222
+ decoded_rows = [_decode_row(value, schema, index) for index, value in enumerate(records[1:])]
223
+
224
+ result: dict[str, pd.Series] = {}
225
+ for column in schema.columns:
226
+ values = [row[column.ordinal] for row in decoded_rows]
227
+ if column.reference:
228
+ mode = modes[column.name]
229
+ presentations = [
230
+ pd.NA if value is None else cast(dict[str, object], value)["presentation"]
231
+ for value in values
232
+ ]
233
+ identifiers = [
234
+ pd.NA if value is None else cast(dict[str, object], value)["uuid"]
235
+ for value in values
236
+ ]
237
+ if mode is ReferenceMode.UUID:
238
+ result[column.name] = pd.Series(identifiers, dtype="object")
239
+ else:
240
+ result[column.name] = pd.Series(presentations, dtype="string")
241
+ if mode is ReferenceMode.BOTH:
242
+ result[column.name + selected_policy.uuid_suffix] = pd.Series(
243
+ identifiers, dtype="object"
244
+ )
245
+ continue
246
+ result[column.name] = _scalar_series(values, column)
247
+
248
+ frame = pd.DataFrame(result)
249
+ frame.attrs["onec_schema"] = schema
250
+ frame.attrs["reference_modes"] = {
251
+ name: mode.value for name, mode in modes.items()
252
+ }
253
+ return frame
254
+
255
+
256
+ def _read_records(payload: bytes) -> list[dict[str, object]]:
257
+ try:
258
+ text = payload.decode("utf-8")
259
+ except UnicodeDecodeError as error:
260
+ raise TableMaterializationError("table payload is not valid UTF-8") from error
261
+ lines = text.splitlines()
262
+ if not lines:
263
+ raise TableMaterializationError("table payload is empty")
264
+ records: list[dict[str, object]] = []
265
+ for index, line in enumerate(lines, start=1):
266
+ try:
267
+ value = json.loads(line)
268
+ except json.JSONDecodeError as error:
269
+ raise TableMaterializationError(
270
+ f"table payload line {index} is not valid JSON"
271
+ ) from error
272
+ if not isinstance(value, dict):
273
+ raise TableMaterializationError(
274
+ f"table payload line {index} is not an object"
275
+ )
276
+ records.append(cast(dict[str, object], value))
277
+ return records
278
+
279
+
280
+ def _read_schema(value: dict[str, object]) -> TableSchema:
281
+ if value.get("record") != "schema" or value.get("version") != 1:
282
+ raise TableMaterializationError("unsupported table schema record")
283
+ raw_columns = value.get("columns")
284
+ if not isinstance(raw_columns, list):
285
+ raise TableMaterializationError("table schema columns are invalid")
286
+ columns: list[TableColumn] = []
287
+ names: set[str] = set()
288
+ for ordinal, raw in enumerate(raw_columns):
289
+ if not isinstance(raw, dict):
290
+ raise TableMaterializationError("table schema column is invalid")
291
+ name = raw.get("name")
292
+ kinds = raw.get("kinds")
293
+ reference = raw.get("reference")
294
+ if (
295
+ not isinstance(name, str)
296
+ or not name
297
+ or raw.get("ordinal") != ordinal
298
+ or not isinstance(kinds, list)
299
+ or not kinds
300
+ or any(not isinstance(kind, str) or not kind for kind in kinds)
301
+ or type(reference) is not bool
302
+ ):
303
+ raise TableMaterializationError(
304
+ f"table schema column {ordinal} is invalid"
305
+ )
306
+ if name in names:
307
+ raise TableMaterializationError(f"duplicate column name: {name}")
308
+ names.add(name)
309
+ columns.append(
310
+ TableColumn(name, ordinal, tuple(cast(list[str], kinds)), cast(bool, reference))
311
+ )
312
+ return TableSchema(1, tuple(columns))
313
+
314
+
315
+ def _mode(value: str | ReferenceMode) -> ReferenceMode:
316
+ try:
317
+ return ReferenceMode(value)
318
+ except ValueError as error:
319
+ raise TableMaterializationError(f"unknown reference mode: {value}") from error
320
+
321
+
322
+ def _validate_policy(
323
+ schema: TableSchema,
324
+ policy: ReferencePolicy,
325
+ ) -> dict[str, ReferenceMode]:
326
+ if not policy.uuid_suffix:
327
+ raise TableMaterializationError("UUID suffix must not be empty")
328
+ default = _mode(policy.refs)
329
+ reference_names = {column.name for column in schema.columns if column.reference}
330
+ overrides = dict(policy.ref_columns or {})
331
+ unknown = set(overrides) - reference_names
332
+ if unknown:
333
+ raise TableMaterializationError(
334
+ "unknown reference column: " + ", ".join(sorted(unknown))
335
+ )
336
+ modes = {
337
+ name: _mode(overrides.get(name, default)) for name in reference_names
338
+ }
339
+ names = {column.name for column in schema.columns}
340
+ for name, mode in modes.items():
341
+ generated = name + policy.uuid_suffix
342
+ if mode is ReferenceMode.BOTH and generated in names:
343
+ raise TableMaterializationError(
344
+ f"generated UUID column collision: {generated}"
345
+ )
346
+ return modes
347
+
348
+
349
+ def _decode_row(
350
+ value: dict[str, object],
351
+ schema: TableSchema,
352
+ row_index: int,
353
+ ) -> list[object]:
354
+ raw_values = value.get("values")
355
+ if value.get("record") != "row" or not isinstance(raw_values, list):
356
+ raise TableMaterializationError(f"row {row_index} is invalid")
357
+ if len(raw_values) != len(schema.columns):
358
+ raise TableMaterializationError(f"row width mismatch at row {row_index}")
359
+ return [
360
+ _decode_cell(cell, schema.columns[index], row_index)
361
+ for index, cell in enumerate(raw_values)
362
+ ]
363
+
364
+
365
+ def _decode_cell(value: object, column: TableColumn, row_index: int) -> object:
366
+ if not isinstance(value, list) or len(value) != 2 or not isinstance(value[0], str):
367
+ raise TableMaterializationError(
368
+ f"invalid cell at row {row_index}, column {column.name}"
369
+ )
370
+ tag, raw = value
371
+ if tag not in _SCALAR_TAGS and tag != "reference":
372
+ raise TableMaterializationError(
373
+ f"unsupported cell tag {tag} at row {row_index}, column {column.name}"
374
+ )
375
+ if tag not in column.kinds:
376
+ raise TableMaterializationError(
377
+ f"cell tag {tag} is absent from schema at row {row_index}, column {column.name}"
378
+ )
379
+ if tag in {"undefined", "null"}:
380
+ if raw is not None:
381
+ raise TableMaterializationError(f"{tag} cell must contain null")
382
+ return None
383
+ if tag == "string":
384
+ if not isinstance(raw, str):
385
+ raise TableMaterializationError("string cell value is invalid")
386
+ return raw
387
+ if tag == "boolean":
388
+ if type(raw) is not bool:
389
+ raise TableMaterializationError("boolean cell value is invalid")
390
+ return raw
391
+ if tag == "number":
392
+ if not isinstance(raw, str):
393
+ raise TableMaterializationError("number cell value is invalid")
394
+ try:
395
+ return Decimal(raw)
396
+ except InvalidOperation as error:
397
+ raise TableMaterializationError("number cell is not canonical decimal") from error
398
+ if tag == "datetime":
399
+ if not isinstance(raw, str):
400
+ raise TableMaterializationError("datetime cell value is invalid")
401
+ try:
402
+ return datetime.fromisoformat(raw)
403
+ except ValueError as error:
404
+ raise TableMaterializationError("datetime cell is not ISO-8601") from error
405
+ if tag == "uuid":
406
+ if not isinstance(raw, str):
407
+ raise TableMaterializationError("UUID cell value is invalid")
408
+ try:
409
+ return UUID(raw)
410
+ except ValueError as error:
411
+ raise TableMaterializationError("UUID cell is invalid") from error
412
+ if not column.reference or not isinstance(raw, dict):
413
+ raise TableMaterializationError("reference cell is invalid")
414
+ required = {"type", "uuid", "presentation", "empty"}
415
+ if set(raw) != required:
416
+ raise TableMaterializationError("reference cell fields are invalid")
417
+ type_name = raw.get("type")
418
+ identifier = raw.get("uuid")
419
+ presentation = raw.get("presentation")
420
+ empty = raw.get("empty")
421
+ if (
422
+ not isinstance(type_name, str)
423
+ or not type_name
424
+ or type(presentation) is not str
425
+ or type(empty) is not bool
426
+ ):
427
+ raise TableMaterializationError("reference cell value is invalid")
428
+ if empty:
429
+ if identifier is not None or presentation != "":
430
+ raise TableMaterializationError("empty reference cell is inconsistent")
431
+ return None
432
+ if not isinstance(identifier, str):
433
+ raise TableMaterializationError("reference UUID is missing")
434
+ try:
435
+ parsed = UUID(identifier)
436
+ except ValueError as error:
437
+ raise TableMaterializationError("reference UUID is invalid") from error
438
+ return {"type": type_name, "uuid": parsed, "presentation": presentation}
439
+
440
+
441
+ def _datetime_series(values: list[object]) -> pd.Series:
442
+ nullable = [None if value is None else value for value in values]
443
+ try:
444
+ return pd.Series(pd.to_datetime(nullable), dtype="datetime64[ns]")
445
+ except (pd.errors.OutOfBoundsDatetime, OverflowError):
446
+ # 1C dates can lie outside pandas' nanosecond range (for example,
447
+ # 0001-01-01 and 3999-12-31). Microseconds cover the full 1C range.
448
+ return pd.Series(nullable, dtype="datetime64[us]")
449
+
450
+
451
+ def _scalar_series(values: list[object], column: TableColumn) -> pd.Series:
452
+ non_missing = [value for value in values if value is not None]
453
+ tags = set(column.kinds) - {"undefined", "null"}
454
+ converted = [pd.NA if value is None else value for value in values]
455
+ if tags == {"string"}:
456
+ return pd.Series(converted, dtype="string")
457
+ if tags == {"boolean"}:
458
+ return pd.Series(converted, dtype="boolean")
459
+ if tags == {"number"} and all(
460
+ isinstance(value, Decimal) and value == value.to_integral()
461
+ for value in non_missing
462
+ ):
463
+ integers = [pd.NA if value is None else int(cast(Decimal, value)) for value in values]
464
+ if all(
465
+ value is pd.NA or -(2**63) <= cast(int, value) < 2**63
466
+ for value in integers
467
+ ):
468
+ return pd.Series(integers, dtype="Int64")
469
+ if tags == {"datetime"}:
470
+ return _datetime_series(values)
471
+ return pd.Series(converted, dtype="object")
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ import re
5
+ from uuid import UUID
6
+
7
+ from onec_runtime.errors import ProtocolError
8
+ from onec_runtime.experiment import bsl_string_literal
9
+ from onec_runtime.table_materialization import (
10
+ TableTransferChunk,
11
+ TableTransferManifest,
12
+ )
13
+
14
+
15
+ _HANDLE = re.compile(r"Контекст\.[^\W\d]\w*\Z", re.UNICODE)
16
+
17
+
18
+ def _require_handle(value: str) -> str:
19
+ if not _HANDLE.fullmatch(value):
20
+ raise ProtocolError(
21
+ "table handle must be one direct persistent Context symbol"
22
+ )
23
+ return value
24
+
25
+
26
+ def _token(value: str) -> str:
27
+ try:
28
+ return str(UUID(value))
29
+ except (ValueError, AttributeError) as error:
30
+ raise ProtocolError("table transfer token is not a UUID") from error
31
+
32
+
33
+ def start_transfer_instruction(
34
+ table_handle: str,
35
+ chunk_size: int,
36
+ runtime_generation: int,
37
+ context_generation: int,
38
+ ) -> str:
39
+ handle = _require_handle(table_handle)
40
+ if type(chunk_size) is not int or chunk_size <= 0:
41
+ raise ProtocolError("table transfer chunk size must be positive")
42
+ if runtime_generation <= 0 or context_generation <= 0:
43
+ raise ProtocolError("table transfer generations must be positive")
44
+ return (
45
+ f"СсылкиМатериализации = RuntimeTableTransferServer.СобратьСсылки("
46
+ f"{handle});\n"
47
+ "ПредставленияМатериализации = "
48
+ "RuntimeTableTransferServer.ПолучитьПредставленияСсылок("
49
+ "СсылкиМатериализации);\n"
50
+ f"МатериализацияТаблицы = RuntimeTableTransferServer.СериализоватьТаблицу("
51
+ f"{handle}, {chunk_size}, ПредставленияМатериализации);\n"
52
+ 'Если Не Контекст.Свойство("RuntimeTableTransfers") Тогда\n'
53
+ ' Контекст.Вставить("RuntimeTableTransfers", Новый Соответствие);\n'
54
+ "КонецЕсли;\n"
55
+ "ТокенМатериализации = Строка(Новый УникальныйИдентификатор());\n"
56
+ "АдресМатериализации = ПоместитьВоВременноеХранилище("
57
+ "МатериализацияТаблицы.Части);\n"
58
+ "ОписаниеМатериализации = Новый Структура;\n"
59
+ 'ОписаниеМатериализации.Вставить("Адрес", АдресМатериализации);\n'
60
+ 'ОписаниеМатериализации.Вставить("Размер", МатериализацияТаблицы.Размер);\n'
61
+ 'ОписаниеМатериализации.Вставить("РазмерСхемы", '
62
+ "МатериализацияТаблицы.РазмерСхемы);\n"
63
+ 'ОписаниеМатериализации.Вставить("КоличествоЧастей", '
64
+ "МатериализацияТаблицы.КоличествоЧастей);\n"
65
+ 'ОписаниеМатериализации.Вставить("ХешДанных", '
66
+ "МатериализацияТаблицы.ХешДанных);\n"
67
+ 'ОписаниеМатериализации.Вставить("ХешСхемы", '
68
+ "МатериализацияТаблицы.ХешСхемы);\n"
69
+ f'ОписаниеМатериализации.Вставить("RuntimeПоколение", '
70
+ f"{runtime_generation});\n"
71
+ f'ОписаниеМатериализации.Вставить("КонтекстПоколение", '
72
+ f"{context_generation});\n"
73
+ "Контекст.RuntimeTableTransfers.Вставить(ТокенМатериализации, "
74
+ "ОписаниеМатериализации);\n"
75
+ "Результат = ТокенМатериализации;"
76
+ )
77
+
78
+
79
+ def get_manifest_instruction(token: str) -> str:
80
+ literal = bsl_string_literal(_token(token))
81
+ return (
82
+ f"ТокенМатериализации = {literal};\n"
83
+ "ОписаниеМатериализации = Контекст.RuntimeTableTransfers.Получить("
84
+ "ТокенМатериализации);\n"
85
+ "Если ОписаниеМатериализации = Неопределено Тогда\n"
86
+ ' ВызватьИсключение "unknown table transfer token";\n'
87
+ "КонецЕсли;\n"
88
+ "Результат = ТокенМатериализации + \"|\" + "
89
+ "Формат(ОписаниеМатериализации.RuntimeПоколение, \"ЧГ=0; ЧДЦ=0\") + \"|\" + "
90
+ "Формат(ОписаниеМатериализации.КонтекстПоколение, \"ЧГ=0; ЧДЦ=0\") + \"|\" + "
91
+ "Формат(ОписаниеМатериализации.Размер, \"ЧГ=0; ЧДЦ=0\") + \"|\" + "
92
+ "Формат(ОписаниеМатериализации.РазмерСхемы, \"ЧГ=0; ЧДЦ=0\") + \"|\" + "
93
+ "Формат(ОписаниеМатериализации.КоличествоЧастей, \"ЧГ=0; ЧДЦ=0\") + \"|\" + "
94
+ "ОписаниеМатериализации.ХешДанных + \"|\" + "
95
+ "ОписаниеМатериализации.ХешСхемы;"
96
+ )
97
+
98
+
99
+ def get_chunk_instruction(token: str, sequence: int) -> str:
100
+ if type(sequence) is not int or sequence < 0:
101
+ raise ProtocolError("table chunk sequence must be non-negative")
102
+ literal = bsl_string_literal(_token(token))
103
+ return (
104
+ f"ТокенМатериализации = {literal};\n"
105
+ "ОписаниеМатериализации = Контекст.RuntimeTableTransfers.Получить("
106
+ "ТокенМатериализации);\n"
107
+ "Если ОписаниеМатериализации = Неопределено Тогда\n"
108
+ ' ВызватьИсключение "unknown table transfer token";\n'
109
+ "КонецЕсли;\n"
110
+ "ЧастиМатериализации = ПолучитьИзВременногоХранилища("
111
+ "ОписаниеМатериализации.Адрес);\n"
112
+ f"ЧастьМатериализации = RuntimeTableTransferServer.ПолучитьЧасть("
113
+ f"ЧастиМатериализации, {sequence});\n"
114
+ f'Результат = "{sequence}|" + ЧастьМатериализации;'
115
+ )
116
+
117
+
118
+ def close_transfer_instruction(token: str) -> str:
119
+ literal = bsl_string_literal(_token(token))
120
+ return (
121
+ f"ТокенМатериализации = {literal};\n"
122
+ "Если Контекст.Свойство(\"RuntimeTableTransfers\") Тогда\n"
123
+ " ОписаниеМатериализации = Контекст.RuntimeTableTransfers.Получить("
124
+ "ТокенМатериализации);\n"
125
+ " Если ОписаниеМатериализации <> Неопределено Тогда\n"
126
+ " УдалитьИзВременногоХранилища(ОписаниеМатериализации.Адрес);\n"
127
+ " Контекст.RuntimeTableTransfers.Удалить(ТокенМатериализации);\n"
128
+ " КонецЕсли;\n"
129
+ "КонецЕсли;\n"
130
+ "Результат = Истина;"
131
+ )
132
+
133
+
134
+ class RuntimeTableTransferBackend:
135
+ def __init__(
136
+ self,
137
+ instruction_executor: Callable[[str], object],
138
+ *,
139
+ runtime_generation: Callable[[], int],
140
+ context_generation: int,
141
+ ) -> None:
142
+ self._execute = instruction_executor
143
+ self._runtime_generation = runtime_generation
144
+ self._context_generation = context_generation
145
+ self._expected_runtime_generation = runtime_generation()
146
+
147
+ def _require_current_runtime(self) -> int:
148
+ current = self._runtime_generation()
149
+ if current != self._expected_runtime_generation:
150
+ raise ProtocolError("table materializer runtime generation is stale")
151
+ return current
152
+
153
+ def start(self, handle: str, chunk_size: int) -> str:
154
+ generation = self._require_current_runtime()
155
+ result = self._execute(
156
+ start_transfer_instruction(
157
+ handle,
158
+ chunk_size,
159
+ generation,
160
+ self._context_generation,
161
+ )
162
+ )
163
+ if not isinstance(result, str):
164
+ raise ProtocolError("table transfer start did not return a token")
165
+ return _token(result)
166
+
167
+ def manifest(self, token: str) -> TableTransferManifest:
168
+ self._require_current_runtime()
169
+ result = self._execute(get_manifest_instruction(token))
170
+ if not isinstance(result, str):
171
+ raise ProtocolError("table transfer manifest is not a string")
172
+ fields = result.split("|")
173
+ if len(fields) != 8:
174
+ raise ProtocolError("table transfer manifest field count is invalid")
175
+ try:
176
+ return TableTransferManifest(
177
+ _token(fields[0]),
178
+ int(fields[1]),
179
+ int(fields[2]),
180
+ int(fields[3]),
181
+ int(fields[4]),
182
+ int(fields[5]),
183
+ fields[6],
184
+ fields[7],
185
+ )
186
+ except ValueError as error:
187
+ raise ProtocolError("table transfer manifest number is invalid") from error
188
+
189
+ def chunk(self, token: str, sequence: int) -> TableTransferChunk:
190
+ self._require_current_runtime()
191
+ result = self._execute(get_chunk_instruction(token, sequence))
192
+ if not isinstance(result, str) or "|" not in result:
193
+ raise ProtocolError("table transfer chunk response is invalid")
194
+ sequence_text, content = result.split("|", 1)
195
+ try:
196
+ observed = int(sequence_text)
197
+ except ValueError as error:
198
+ raise ProtocolError("table transfer chunk sequence is invalid") from error
199
+ return TableTransferChunk(observed, content)
200
+
201
+ def close(self, token: str) -> None:
202
+ if self._execute(close_transfer_instruction(token)) is not True:
203
+ raise ProtocolError("table transfer close acknowledgement is invalid")