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,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")
|