qplotter 1.6.0b3__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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +63 -0
- qplot/_metadata.py +12 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +573 -0
- qplot/configuration/config_schema.json +319 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +579 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2330 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2561 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +127 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +868 -0
- qplot/datahandling/trusted_heatmap.py +241 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1894 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1733 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +114 -0
- qplot/tools/general.py +134 -0
- qplot/tools/heatmap_geometry.py +381 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +625 -0
- qplot/tools/sample_statistics.py +56 -0
- qplot/tools/worker.py +2787 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4904 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_dragdrop.py +71 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +169 -0
- qplot/windows/_native_averaging.py +173 -0
- qplot/windows/_native_transforms.py +389 -0
- qplot/windows/_plot1d_snap.py +608 -0
- qplot/windows/_plot1d_traces.py +2183 -0
- qplot/windows/_plot2d_colorbar.py +1126 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2197 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +1993 -0
- qplot/windows/_plot_actions.py +2695 -0
- qplot/windows/_plot_appearance.py +276 -0
- qplot/windows/_plot_axis_scaling.py +1485 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +110 -0
- qplot/windows/_plot_marquee.py +656 -0
- qplot/windows/_plot_refresh.py +942 -0
- qplot/windows/_plot_state.py +239 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +635 -0
- qplot/windows/_refresh_interval.py +70 -0
- qplot/windows/_run_controls.py +828 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +531 -0
- qplot/windows/_subplots/subplot2d.py +918 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +413 -0
- qplot/windows/main.py +1635 -0
- qplot/windows/plot1d.py +311 -0
- qplot/windows/plot2d.py +1901 -0
- qplotter-1.6.0b3.dist-info/METADATA +318 -0
- qplotter-1.6.0b3.dist-info/RECORD +119 -0
- qplotter-1.6.0b3.dist-info/WHEEL +5 -0
- qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
"""Bounded, Qt-independent decoding of selected-run snapshot JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from json import decoder as json_decoder
|
|
13
|
+
from typing import Literal, TypeAlias, cast
|
|
14
|
+
|
|
15
|
+
TRUSTED_SNAPSHOT_MAX_INPUT_BYTES = 4 * 1024 * 1024
|
|
16
|
+
TRUSTED_SNAPSHOT_MAX_DEPTH = 32
|
|
17
|
+
TRUSTED_SNAPSHOT_MAX_CONTAINER_ITEMS = TRUSTED_SNAPSHOT_MAX_INPUT_BYTES
|
|
18
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN = 128
|
|
19
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION = 127
|
|
20
|
+
TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES = 32 * 1024
|
|
21
|
+
TRUSTED_SNAPSHOT_LOAD_MORE_TEXT = "Load more…"
|
|
22
|
+
TRUSTED_SNAPSHOT_MAX_RENDERED_NODES = TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN
|
|
23
|
+
TRUSTED_SNAPSHOT_MAX_RENDERED_TEXT_BYTES = TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES
|
|
24
|
+
TRUSTED_SNAPSHOT_MAX_NODE_KEY_BYTES = 256
|
|
25
|
+
TRUSTED_SNAPSHOT_MAX_NODE_VALUE_BYTES = 1_024
|
|
26
|
+
TRUSTED_SNAPSHOT_MAX_TOOLTIP_BYTES = 2 * 1024
|
|
27
|
+
TRUSTED_SNAPSHOT_MAX_SCALAR_BYTES = 128 * 1024
|
|
28
|
+
TRUSTED_SNAPSHOT_MAX_PARAMETER_VIEWS = 256
|
|
29
|
+
TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES = 512
|
|
30
|
+
|
|
31
|
+
_MARKER_KEY = "[truncated]"
|
|
32
|
+
_VIEW_FULL_SUFFIX = " [view full]"
|
|
33
|
+
_PAGE_MAX_DATA_TEXT_BYTES = TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES - len(
|
|
34
|
+
TRUSTED_SNAPSHOT_LOAD_MORE_TEXT.encode("utf-8")
|
|
35
|
+
)
|
|
36
|
+
_NUMBER = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?")
|
|
37
|
+
_SCAN_STRING = cast(
|
|
38
|
+
Callable[[str, int, bool], tuple[str, int]],
|
|
39
|
+
json_decoder.scanstring, # type: ignore[attr-defined]
|
|
40
|
+
)
|
|
41
|
+
_PARAMETER_FIELDS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"name",
|
|
44
|
+
"full_name",
|
|
45
|
+
"label",
|
|
46
|
+
"unit",
|
|
47
|
+
"post_delay",
|
|
48
|
+
"instrument_name",
|
|
49
|
+
"instrument",
|
|
50
|
+
"value",
|
|
51
|
+
"raw_value",
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
SnapshotScalar: TypeAlias = None | bool | int | float | str
|
|
56
|
+
FrozenSnapshotFields: TypeAlias = tuple[tuple[str, SnapshotScalar], ...]
|
|
57
|
+
SnapshotStatus: TypeAlias = Literal[
|
|
58
|
+
"available",
|
|
59
|
+
"empty",
|
|
60
|
+
"truncated",
|
|
61
|
+
"malformed",
|
|
62
|
+
"unavailable",
|
|
63
|
+
]
|
|
64
|
+
SnapshotOmissionKind: TypeAlias = Literal[
|
|
65
|
+
"payload_limit",
|
|
66
|
+
"detail_budget",
|
|
67
|
+
"changed_during_read",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class TrustedSnapshotOmission:
|
|
73
|
+
"""Why a present snapshot payload was not delivered to the normalizer."""
|
|
74
|
+
|
|
75
|
+
kind: SnapshotOmissionKind
|
|
76
|
+
input_bytes: int | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class TrustedSnapshotParameterView:
|
|
81
|
+
"""Bounded snapshot fields used by the selected-run parameter table."""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
fields: FrozenSnapshotFields
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _SnapshotSyntaxError(ValueError):
|
|
88
|
+
def __init__(self, position: int, message: str) -> None:
|
|
89
|
+
super().__init__(message)
|
|
90
|
+
self.position = max(0, position)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parameter_location(
|
|
94
|
+
path: tuple[str, ...],
|
|
95
|
+
) -> tuple[tuple[str, ...], str, str] | None:
|
|
96
|
+
if len(path) == 3 and path[0] == "parameters":
|
|
97
|
+
return path[:2], path[1], path[2]
|
|
98
|
+
if len(path) == 4 and path[:2] == ("station", "parameters"):
|
|
99
|
+
return path[:3], path[2], path[3]
|
|
100
|
+
if (
|
|
101
|
+
len(path) == 6
|
|
102
|
+
and path[:2] == ("station", "instruments")
|
|
103
|
+
and path[3] == "parameters"
|
|
104
|
+
):
|
|
105
|
+
return path[:5], path[4], path[5]
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _truncate_utf8(text: str, limit: int) -> tuple[str, bool]:
|
|
110
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
111
|
+
if len(encoded) <= limit:
|
|
112
|
+
return text, False
|
|
113
|
+
if limit <= 0:
|
|
114
|
+
return "", True
|
|
115
|
+
marker = b"..." if limit >= 3 else b""
|
|
116
|
+
prefix = encoded[: max(0, limit - len(marker))]
|
|
117
|
+
while prefix:
|
|
118
|
+
try:
|
|
119
|
+
decoded = prefix.decode("utf-8")
|
|
120
|
+
break
|
|
121
|
+
except UnicodeDecodeError as error:
|
|
122
|
+
prefix = prefix[: error.start]
|
|
123
|
+
else:
|
|
124
|
+
decoded = ""
|
|
125
|
+
return decoded + marker.decode("ascii"), True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _snapshot_value_preview(text: str) -> tuple[str, bool]:
|
|
129
|
+
"""Bound one scalar preview while preserving its exact-value affordance."""
|
|
130
|
+
|
|
131
|
+
if len(text.encode("utf-8")) <= TRUSTED_SNAPSHOT_MAX_NODE_VALUE_BYTES:
|
|
132
|
+
return text, False
|
|
133
|
+
prefix_limit = TRUSTED_SNAPSHOT_MAX_NODE_VALUE_BYTES - len(
|
|
134
|
+
_VIEW_FULL_SUFFIX.encode("utf-8")
|
|
135
|
+
)
|
|
136
|
+
prefix, _shortened = _truncate_utf8(text, prefix_limit)
|
|
137
|
+
return prefix + _VIEW_FULL_SUFFIX, True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True, slots=True)
|
|
141
|
+
class TrustedSnapshotContainerHandle:
|
|
142
|
+
"""Opaque source-bound reference to one JSON object or array."""
|
|
143
|
+
|
|
144
|
+
token: str
|
|
145
|
+
_session: str = field(repr=False)
|
|
146
|
+
_start: int = field(repr=False)
|
|
147
|
+
_end: int = field(repr=False)
|
|
148
|
+
_depth: int = field(repr=False)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class TrustedSnapshotContinuation:
|
|
153
|
+
"""Opaque source- and parent-bound cursor for a subsequent child page."""
|
|
154
|
+
|
|
155
|
+
token: str
|
|
156
|
+
_session: str = field(repr=False)
|
|
157
|
+
_parent_token: str = field(repr=False)
|
|
158
|
+
_position: int = field(repr=False)
|
|
159
|
+
_item_index: int = field(repr=False)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True, slots=True)
|
|
163
|
+
class TrustedSnapshotFullValueHandle:
|
|
164
|
+
"""Opaque source-bound reference to an exact shortened key or scalar."""
|
|
165
|
+
|
|
166
|
+
token: str
|
|
167
|
+
_session: str = field(repr=False)
|
|
168
|
+
_start: int = field(repr=False)
|
|
169
|
+
_end: int = field(repr=False)
|
|
170
|
+
_kind: str = field(repr=False)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True, slots=True)
|
|
174
|
+
class TrustedSnapshotNode:
|
|
175
|
+
"""One bounded direct-child row returned by the lazy scanner."""
|
|
176
|
+
|
|
177
|
+
key: str
|
|
178
|
+
value: str
|
|
179
|
+
parent_index: int | None = None
|
|
180
|
+
tooltip: str = ""
|
|
181
|
+
container_handle: TrustedSnapshotContainerHandle | None = None
|
|
182
|
+
full_key_handle: TrustedSnapshotFullValueHandle | None = None
|
|
183
|
+
full_value_handle: TrustedSnapshotFullValueHandle | None = None
|
|
184
|
+
key_shortened: bool = False
|
|
185
|
+
value_shortened: bool = False
|
|
186
|
+
key_utf8_bytes: int = 0
|
|
187
|
+
value_utf8_bytes: int = 0
|
|
188
|
+
source_position: int = 0
|
|
189
|
+
source_key_bytes: int = 0
|
|
190
|
+
source_value_bytes: int = 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass(frozen=True, slots=True)
|
|
194
|
+
class TrustedSnapshotPage:
|
|
195
|
+
"""A bounded direct-children page which echoes its request identity."""
|
|
196
|
+
|
|
197
|
+
parent_handle: TrustedSnapshotContainerHandle | None
|
|
198
|
+
request_continuation: TrustedSnapshotContinuation | None
|
|
199
|
+
nodes: tuple[TrustedSnapshotNode, ...]
|
|
200
|
+
continuation: TrustedSnapshotContinuation | None
|
|
201
|
+
rendered_text_bytes: int
|
|
202
|
+
message: str
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def parent(self) -> TrustedSnapshotContainerHandle | None:
|
|
206
|
+
return self.parent_handle
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def cursor(self) -> TrustedSnapshotContinuation | None:
|
|
210
|
+
return self.request_continuation
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True)
|
|
214
|
+
class TrustedSnapshotFullValue:
|
|
215
|
+
"""One exact value resolved away from the Qt owner thread."""
|
|
216
|
+
|
|
217
|
+
text: str
|
|
218
|
+
utf8_bytes: int
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True, slots=True)
|
|
222
|
+
class TrustedSnapshotView:
|
|
223
|
+
"""The initial root page and its exact, Qt-free retained source."""
|
|
224
|
+
|
|
225
|
+
page: TrustedSnapshotPage
|
|
226
|
+
parameters: tuple[TrustedSnapshotParameterView, ...]
|
|
227
|
+
status: SnapshotStatus
|
|
228
|
+
message: str
|
|
229
|
+
input_bytes: int
|
|
230
|
+
session_token: str
|
|
231
|
+
root_handle: TrustedSnapshotContainerHandle | None
|
|
232
|
+
continuation: TrustedSnapshotContinuation | None
|
|
233
|
+
source: TrustedSnapshotSource | None = field(repr=False, compare=False)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def nodes(self) -> tuple[TrustedSnapshotNode, ...]:
|
|
237
|
+
"""Compatibility alias for the initial page's nodes."""
|
|
238
|
+
|
|
239
|
+
return self.page.nodes
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def initial_page(self) -> TrustedSnapshotPage:
|
|
243
|
+
return self.page
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def session(self) -> str:
|
|
247
|
+
return self.session_token
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def root(self) -> TrustedSnapshotContainerHandle | None:
|
|
251
|
+
return self.root_handle
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def cursor(self) -> TrustedSnapshotContinuation | None:
|
|
255
|
+
return self.continuation
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True, slots=True)
|
|
259
|
+
class _LazyScalar:
|
|
260
|
+
start: int
|
|
261
|
+
end: int
|
|
262
|
+
kind: str
|
|
263
|
+
value: SnapshotScalar
|
|
264
|
+
display: str
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True, slots=True)
|
|
268
|
+
class _LazyValidated:
|
|
269
|
+
start: int
|
|
270
|
+
end: int
|
|
271
|
+
container: bool
|
|
272
|
+
empty_container: bool
|
|
273
|
+
parameters: tuple[TrustedSnapshotParameterView, ...]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _LazySnapshotLimit(RuntimeError):
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _lazy_cancel(cancel_check: Callable[[], None] | None) -> None:
|
|
281
|
+
if cancel_check is not None:
|
|
282
|
+
cancel_check()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _lazy_space(source: str, position: int) -> int:
|
|
286
|
+
while position < len(source) and source[position] in " \t\r\n":
|
|
287
|
+
position += 1
|
|
288
|
+
return position
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _lazy_scan_string(source: str, position: int) -> tuple[str, int]:
|
|
292
|
+
try:
|
|
293
|
+
value, end = _SCAN_STRING(source, position + 1, True)
|
|
294
|
+
value_bytes = len(value.encode("utf-8"))
|
|
295
|
+
except (UnicodeDecodeError, UnicodeEncodeError, ValueError) as error:
|
|
296
|
+
raise _SnapshotSyntaxError(position, "The JSON string is invalid.") from error
|
|
297
|
+
if value_bytes > TRUSTED_SNAPSHOT_MAX_SCALAR_BYTES:
|
|
298
|
+
raise _LazySnapshotLimit(
|
|
299
|
+
"Snapshot scalar text exceeds the 131072-byte inspection limit."
|
|
300
|
+
)
|
|
301
|
+
return value, end
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _lazy_scalar(source: str, position: int) -> _LazyScalar:
|
|
305
|
+
if position >= len(source):
|
|
306
|
+
raise _SnapshotSyntaxError(position, "A JSON value is missing.")
|
|
307
|
+
if source[position] == '"':
|
|
308
|
+
string_value, end = _lazy_scan_string(source, position)
|
|
309
|
+
return _LazyScalar(position, end, "string", string_value, string_value)
|
|
310
|
+
literals: tuple[tuple[str, SnapshotScalar, str, str], ...] = (
|
|
311
|
+
("true", True, "True", "true"),
|
|
312
|
+
("false", False, "False", "false"),
|
|
313
|
+
("null", None, "", "null"),
|
|
314
|
+
)
|
|
315
|
+
for literal, literal_value, display, kind in literals:
|
|
316
|
+
if source.startswith(literal, position):
|
|
317
|
+
return _LazyScalar(
|
|
318
|
+
position,
|
|
319
|
+
position + len(literal),
|
|
320
|
+
kind,
|
|
321
|
+
literal_value,
|
|
322
|
+
display,
|
|
323
|
+
)
|
|
324
|
+
# QCoDeS's NumpyJSONEncoder emits these non-standard numeric tokens.
|
|
325
|
+
# Preserve their spelling as text across the finite-only IPC boundary,
|
|
326
|
+
# including when a nested value is read later through lazy pagination.
|
|
327
|
+
for literal in ("NaN", "Infinity", "-Infinity"):
|
|
328
|
+
if source.startswith(literal, position):
|
|
329
|
+
end = position + len(literal)
|
|
330
|
+
if end == len(source) or source[end] in " \t\r\n,]}":
|
|
331
|
+
return _LazyScalar(position, end, "number", literal, literal)
|
|
332
|
+
raise _SnapshotSyntaxError(end, "The JSON value is invalid.")
|
|
333
|
+
match = _NUMBER.match(source, position)
|
|
334
|
+
if match is None:
|
|
335
|
+
raise _SnapshotSyntaxError(position, "The JSON value is invalid.")
|
|
336
|
+
number_text = match.group(0)
|
|
337
|
+
if len(number_text.encode("utf-8")) > TRUSTED_SNAPSHOT_MAX_SCALAR_BYTES:
|
|
338
|
+
raise _LazySnapshotLimit(
|
|
339
|
+
"Snapshot scalar text exceeds the 131072-byte inspection limit."
|
|
340
|
+
)
|
|
341
|
+
number_value: SnapshotScalar = number_text
|
|
342
|
+
if len(number_text) <= 128:
|
|
343
|
+
try:
|
|
344
|
+
parsed: int | float = (
|
|
345
|
+
float(number_text)
|
|
346
|
+
if any(marker in number_text for marker in ".eE")
|
|
347
|
+
else int(number_text)
|
|
348
|
+
)
|
|
349
|
+
if not isinstance(parsed, float) or math.isfinite(parsed):
|
|
350
|
+
number_value = parsed
|
|
351
|
+
except (OverflowError, ValueError):
|
|
352
|
+
pass
|
|
353
|
+
return _LazyScalar(position, match.end(), "number", number_value, number_text)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _LazySnapshotValidator:
|
|
357
|
+
"""Validate every token while retaining no general-purpose Python tree."""
|
|
358
|
+
|
|
359
|
+
def __init__(
|
|
360
|
+
self,
|
|
361
|
+
source: str,
|
|
362
|
+
cancel_check: Callable[[], None] | None,
|
|
363
|
+
) -> None:
|
|
364
|
+
self.source = source
|
|
365
|
+
self.cancel_check = cancel_check
|
|
366
|
+
self.container_items = 0
|
|
367
|
+
self.parameter_fields: dict[tuple[str, ...], dict[str, SnapshotScalar]] = {}
|
|
368
|
+
self.parameter_names: dict[tuple[str, ...], str] = {}
|
|
369
|
+
|
|
370
|
+
def validate(self) -> _LazyValidated:
|
|
371
|
+
_lazy_cancel(self.cancel_check)
|
|
372
|
+
start = _lazy_space(self.source, 0)
|
|
373
|
+
if start >= len(self.source):
|
|
374
|
+
raise _SnapshotSyntaxError(start, "Snapshot JSON is empty.")
|
|
375
|
+
end = self._value(start, path=(), depth=0)
|
|
376
|
+
trailing = _lazy_space(self.source, end)
|
|
377
|
+
if trailing != len(self.source):
|
|
378
|
+
raise _SnapshotSyntaxError(
|
|
379
|
+
trailing, "Unexpected content follows the JSON value."
|
|
380
|
+
)
|
|
381
|
+
_lazy_cancel(self.cancel_check)
|
|
382
|
+
container = self.source[start] in "{["
|
|
383
|
+
child = _lazy_space(self.source, start + 1) if container else start
|
|
384
|
+
empty = container and child < end and self.source[child] in "}]"
|
|
385
|
+
return _LazyValidated(
|
|
386
|
+
start,
|
|
387
|
+
end,
|
|
388
|
+
container,
|
|
389
|
+
empty,
|
|
390
|
+
self._parameters(),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def _count(self) -> None:
|
|
394
|
+
self.container_items += 1
|
|
395
|
+
if self.container_items > TRUSTED_SNAPSHOT_MAX_CONTAINER_ITEMS:
|
|
396
|
+
raise _LazySnapshotLimit(
|
|
397
|
+
"Snapshot containers exceed the bounded inspection limit."
|
|
398
|
+
)
|
|
399
|
+
if self.container_items % 4_096 == 0:
|
|
400
|
+
_lazy_cancel(self.cancel_check)
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def _depth(depth: int) -> None:
|
|
404
|
+
if depth > TRUSTED_SNAPSHOT_MAX_DEPTH:
|
|
405
|
+
raise _LazySnapshotLimit(
|
|
406
|
+
"Snapshot nesting exceeds the 32-level display limit."
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def _value(
|
|
410
|
+
self,
|
|
411
|
+
position: int,
|
|
412
|
+
*,
|
|
413
|
+
path: tuple[str, ...] | None,
|
|
414
|
+
depth: int,
|
|
415
|
+
) -> int:
|
|
416
|
+
position = _lazy_space(self.source, position)
|
|
417
|
+
if position >= len(self.source):
|
|
418
|
+
raise _SnapshotSyntaxError(position, "A JSON value is missing.")
|
|
419
|
+
if self.source[position] == "{":
|
|
420
|
+
return self._object(position, path=path, depth=depth + 1)
|
|
421
|
+
if self.source[position] == "[":
|
|
422
|
+
return self._array(position, depth=depth + 1)
|
|
423
|
+
scalar = _lazy_scalar(self.source, position)
|
|
424
|
+
self._record(path, scalar.value)
|
|
425
|
+
return scalar.end
|
|
426
|
+
|
|
427
|
+
def _object(
|
|
428
|
+
self,
|
|
429
|
+
position: int,
|
|
430
|
+
*,
|
|
431
|
+
path: tuple[str, ...] | None,
|
|
432
|
+
depth: int,
|
|
433
|
+
) -> int:
|
|
434
|
+
self._depth(depth)
|
|
435
|
+
position = _lazy_space(self.source, position + 1)
|
|
436
|
+
if position < len(self.source) and self.source[position] == "}":
|
|
437
|
+
return position + 1
|
|
438
|
+
while True:
|
|
439
|
+
self._count()
|
|
440
|
+
if position >= len(self.source) or self.source[position] != '"':
|
|
441
|
+
raise _SnapshotSyntaxError(position, "An object key must be a string.")
|
|
442
|
+
key, position = _lazy_scan_string(self.source, position)
|
|
443
|
+
position = _lazy_space(self.source, position)
|
|
444
|
+
if position >= len(self.source) or self.source[position] != ":":
|
|
445
|
+
raise _SnapshotSyntaxError(position, "An object key needs a value.")
|
|
446
|
+
child_path = (*path, key) if path is not None and len(path) < 6 else None
|
|
447
|
+
position = self._value(position + 1, path=child_path, depth=depth)
|
|
448
|
+
position = _lazy_space(self.source, position)
|
|
449
|
+
if position >= len(self.source):
|
|
450
|
+
raise _SnapshotSyntaxError(position, "An object is not closed.")
|
|
451
|
+
if self.source[position] == "}":
|
|
452
|
+
return position + 1
|
|
453
|
+
if self.source[position] != ",":
|
|
454
|
+
raise _SnapshotSyntaxError(position, "Object members need a comma.")
|
|
455
|
+
position = _lazy_space(self.source, position + 1)
|
|
456
|
+
|
|
457
|
+
def _array(self, position: int, *, depth: int) -> int:
|
|
458
|
+
self._depth(depth)
|
|
459
|
+
position = _lazy_space(self.source, position + 1)
|
|
460
|
+
if position < len(self.source) and self.source[position] == "]":
|
|
461
|
+
return position + 1
|
|
462
|
+
while True:
|
|
463
|
+
self._count()
|
|
464
|
+
position = self._value(position, path=None, depth=depth)
|
|
465
|
+
position = _lazy_space(self.source, position)
|
|
466
|
+
if position >= len(self.source):
|
|
467
|
+
raise _SnapshotSyntaxError(position, "An array is not closed.")
|
|
468
|
+
if self.source[position] == "]":
|
|
469
|
+
return position + 1
|
|
470
|
+
if self.source[position] != ",":
|
|
471
|
+
raise _SnapshotSyntaxError(position, "Array values need a comma.")
|
|
472
|
+
position = _lazy_space(self.source, position + 1)
|
|
473
|
+
|
|
474
|
+
def _record(
|
|
475
|
+
self,
|
|
476
|
+
path: tuple[str, ...] | None,
|
|
477
|
+
value: SnapshotScalar,
|
|
478
|
+
) -> None:
|
|
479
|
+
if path is None or (location := _parameter_location(path)) is None:
|
|
480
|
+
return
|
|
481
|
+
raw_identity, raw_name, parameter_field = location
|
|
482
|
+
if parameter_field not in _PARAMETER_FIELDS:
|
|
483
|
+
return
|
|
484
|
+
identity = tuple(
|
|
485
|
+
_truncate_utf8(part, TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES)[0]
|
|
486
|
+
for part in raw_identity
|
|
487
|
+
)
|
|
488
|
+
parameter_name = _truncate_utf8(
|
|
489
|
+
raw_name, TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES
|
|
490
|
+
)[0]
|
|
491
|
+
if identity not in self.parameter_fields:
|
|
492
|
+
if len(self.parameter_fields) >= TRUSTED_SNAPSHOT_MAX_PARAMETER_VIEWS:
|
|
493
|
+
return
|
|
494
|
+
self.parameter_fields[identity] = {}
|
|
495
|
+
self.parameter_names[identity] = parameter_name
|
|
496
|
+
if isinstance(value, str):
|
|
497
|
+
value = _truncate_utf8(value, TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES)[0]
|
|
498
|
+
self.parameter_fields[identity][parameter_field] = value
|
|
499
|
+
|
|
500
|
+
def _parameters(self) -> tuple[TrustedSnapshotParameterView, ...]:
|
|
501
|
+
views: list[TrustedSnapshotParameterView] = []
|
|
502
|
+
seen: set[str] = set()
|
|
503
|
+
for identity, fields in self.parameter_fields.items():
|
|
504
|
+
aliases = [self.parameter_names[identity]]
|
|
505
|
+
aliases.extend(
|
|
506
|
+
value
|
|
507
|
+
for name in ("name", "full_name")
|
|
508
|
+
if isinstance((value := fields.get(name)), str) and value
|
|
509
|
+
)
|
|
510
|
+
frozen: FrozenSnapshotFields = tuple(fields.items())
|
|
511
|
+
for alias in aliases:
|
|
512
|
+
alias = _truncate_utf8(
|
|
513
|
+
alias, TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES
|
|
514
|
+
)[0]
|
|
515
|
+
if not alias or alias in seen:
|
|
516
|
+
continue
|
|
517
|
+
seen.add(alias)
|
|
518
|
+
views.append(TrustedSnapshotParameterView(alias, frozen))
|
|
519
|
+
if len(views) >= TRUSTED_SNAPSHOT_MAX_PARAMETER_VIEWS:
|
|
520
|
+
return tuple(views)
|
|
521
|
+
return tuple(views)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _lazy_skip_value(
|
|
525
|
+
source: str,
|
|
526
|
+
position: int,
|
|
527
|
+
*,
|
|
528
|
+
depth: int,
|
|
529
|
+
cancel_check: Callable[[], None] | None,
|
|
530
|
+
) -> int:
|
|
531
|
+
"""Find one value's end without retaining any descendant values."""
|
|
532
|
+
|
|
533
|
+
position = _lazy_space(source, position)
|
|
534
|
+
if position >= len(source):
|
|
535
|
+
raise _SnapshotSyntaxError(position, "A JSON value is missing.")
|
|
536
|
+
token = source[position]
|
|
537
|
+
if token not in "{[":
|
|
538
|
+
return _lazy_scalar(source, position).end
|
|
539
|
+
depth += 1
|
|
540
|
+
if depth > TRUSTED_SNAPSHOT_MAX_DEPTH:
|
|
541
|
+
raise _LazySnapshotLimit("Snapshot nesting exceeds the 32-level display limit.")
|
|
542
|
+
closing = "}" if token == "{" else "]"
|
|
543
|
+
position = _lazy_space(source, position + 1)
|
|
544
|
+
if position < len(source) and source[position] == closing:
|
|
545
|
+
return position + 1
|
|
546
|
+
item_index = 0
|
|
547
|
+
while True:
|
|
548
|
+
item_index += 1
|
|
549
|
+
if item_index % 4_096 == 0:
|
|
550
|
+
_lazy_cancel(cancel_check)
|
|
551
|
+
if token == "{":
|
|
552
|
+
if position >= len(source) or source[position] != '"':
|
|
553
|
+
raise _SnapshotSyntaxError(position, "An object key must be a string.")
|
|
554
|
+
_key, position = _lazy_scan_string(source, position)
|
|
555
|
+
position = _lazy_space(source, position)
|
|
556
|
+
if position >= len(source) or source[position] != ":":
|
|
557
|
+
raise _SnapshotSyntaxError(position, "An object key needs a value.")
|
|
558
|
+
position += 1
|
|
559
|
+
position = _lazy_skip_value(
|
|
560
|
+
source,
|
|
561
|
+
position,
|
|
562
|
+
depth=depth,
|
|
563
|
+
cancel_check=cancel_check,
|
|
564
|
+
)
|
|
565
|
+
position = _lazy_space(source, position)
|
|
566
|
+
if position >= len(source):
|
|
567
|
+
raise _SnapshotSyntaxError(position, "A container is not closed.")
|
|
568
|
+
if source[position] == closing:
|
|
569
|
+
return position + 1
|
|
570
|
+
if source[position] != ",":
|
|
571
|
+
raise _SnapshotSyntaxError(position, "Container items need a comma.")
|
|
572
|
+
position = _lazy_space(source, position + 1)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@dataclass(frozen=True, slots=True)
|
|
576
|
+
class TrustedSnapshotSource:
|
|
577
|
+
"""One exact bounded source retained outside Qt's item model."""
|
|
578
|
+
|
|
579
|
+
session_token: str
|
|
580
|
+
input_bytes: int
|
|
581
|
+
root: TrustedSnapshotContainerHandle | None
|
|
582
|
+
parameters: tuple[TrustedSnapshotParameterView, ...]
|
|
583
|
+
_text: str = field(repr=False, compare=False)
|
|
584
|
+
_secret: bytes = field(repr=False, compare=False)
|
|
585
|
+
_root_scalar: _LazyScalar | None = field(repr=False, compare=False)
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def session(self) -> str:
|
|
589
|
+
return self.session_token
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def _create(
|
|
593
|
+
cls,
|
|
594
|
+
text: str,
|
|
595
|
+
input_bytes: int,
|
|
596
|
+
validated: _LazyValidated,
|
|
597
|
+
) -> TrustedSnapshotSource:
|
|
598
|
+
source = cls(
|
|
599
|
+
session_token=secrets.token_hex(16),
|
|
600
|
+
input_bytes=input_bytes,
|
|
601
|
+
root=None,
|
|
602
|
+
parameters=validated.parameters,
|
|
603
|
+
_text=text,
|
|
604
|
+
_secret=secrets.token_bytes(32),
|
|
605
|
+
_root_scalar=None,
|
|
606
|
+
)
|
|
607
|
+
if validated.container:
|
|
608
|
+
object.__setattr__(
|
|
609
|
+
source,
|
|
610
|
+
"root",
|
|
611
|
+
source._container(validated.start, validated.end, depth=1),
|
|
612
|
+
)
|
|
613
|
+
else:
|
|
614
|
+
object.__setattr__(
|
|
615
|
+
source,
|
|
616
|
+
"_root_scalar",
|
|
617
|
+
_lazy_scalar(text, validated.start),
|
|
618
|
+
)
|
|
619
|
+
return source
|
|
620
|
+
|
|
621
|
+
def _signature(self, kind: str, *parts: object) -> str:
|
|
622
|
+
payload = repr((kind, self.session, *parts)).encode("ascii")
|
|
623
|
+
return hmac.new(self._secret, payload, hashlib.sha256).hexdigest()
|
|
624
|
+
|
|
625
|
+
def _container(
|
|
626
|
+
self,
|
|
627
|
+
start: int,
|
|
628
|
+
end: int,
|
|
629
|
+
*,
|
|
630
|
+
depth: int,
|
|
631
|
+
) -> TrustedSnapshotContainerHandle:
|
|
632
|
+
return TrustedSnapshotContainerHandle(
|
|
633
|
+
self._signature("container", start, end, depth),
|
|
634
|
+
self.session,
|
|
635
|
+
start,
|
|
636
|
+
end,
|
|
637
|
+
depth,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def _next(
|
|
641
|
+
self,
|
|
642
|
+
parent: TrustedSnapshotContainerHandle,
|
|
643
|
+
position: int,
|
|
644
|
+
item_index: int,
|
|
645
|
+
) -> TrustedSnapshotContinuation:
|
|
646
|
+
return TrustedSnapshotContinuation(
|
|
647
|
+
self._signature("continuation", parent.token, position, item_index),
|
|
648
|
+
self.session,
|
|
649
|
+
parent.token,
|
|
650
|
+
position,
|
|
651
|
+
item_index,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
def _full(
|
|
655
|
+
self,
|
|
656
|
+
start: int,
|
|
657
|
+
end: int,
|
|
658
|
+
kind: str,
|
|
659
|
+
) -> TrustedSnapshotFullValueHandle:
|
|
660
|
+
return TrustedSnapshotFullValueHandle(
|
|
661
|
+
self._signature("full", start, end, kind),
|
|
662
|
+
self.session,
|
|
663
|
+
start,
|
|
664
|
+
end,
|
|
665
|
+
kind,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def _check_parent(self, parent: TrustedSnapshotContainerHandle) -> None:
|
|
669
|
+
if not isinstance(parent, TrustedSnapshotContainerHandle):
|
|
670
|
+
raise TypeError("parent must be a TrustedSnapshotContainerHandle.")
|
|
671
|
+
expected = self._signature(
|
|
672
|
+
"container", parent._start, parent._end, parent._depth
|
|
673
|
+
)
|
|
674
|
+
if (
|
|
675
|
+
parent._session != self.session
|
|
676
|
+
or not secrets.compare_digest(parent.token, expected)
|
|
677
|
+
or not 0 <= parent._start < parent._end <= len(self._text)
|
|
678
|
+
or self._text[parent._start] not in "{["
|
|
679
|
+
):
|
|
680
|
+
raise ValueError("The snapshot container handle is stale or foreign.")
|
|
681
|
+
|
|
682
|
+
def _check_cursor(
|
|
683
|
+
self,
|
|
684
|
+
parent: TrustedSnapshotContainerHandle,
|
|
685
|
+
cursor: TrustedSnapshotContinuation,
|
|
686
|
+
) -> None:
|
|
687
|
+
if not isinstance(cursor, TrustedSnapshotContinuation):
|
|
688
|
+
raise TypeError("cursor must be a TrustedSnapshotContinuation or None.")
|
|
689
|
+
expected = self._signature(
|
|
690
|
+
"continuation",
|
|
691
|
+
parent.token,
|
|
692
|
+
cursor._position,
|
|
693
|
+
cursor._item_index,
|
|
694
|
+
)
|
|
695
|
+
if (
|
|
696
|
+
cursor._session != self.session
|
|
697
|
+
or cursor._parent_token != parent.token
|
|
698
|
+
or not secrets.compare_digest(cursor.token, expected)
|
|
699
|
+
or not parent._start < cursor._position < parent._end
|
|
700
|
+
or cursor._item_index < 0
|
|
701
|
+
):
|
|
702
|
+
raise ValueError("The snapshot continuation is stale or foreign.")
|
|
703
|
+
|
|
704
|
+
def page(
|
|
705
|
+
self,
|
|
706
|
+
parent: TrustedSnapshotContainerHandle,
|
|
707
|
+
cursor: TrustedSnapshotContinuation | None,
|
|
708
|
+
*,
|
|
709
|
+
cancel_check: Callable[[], None] | None = None,
|
|
710
|
+
) -> TrustedSnapshotPage:
|
|
711
|
+
"""Return one page of direct children without constructing a subtree."""
|
|
712
|
+
|
|
713
|
+
self._check_parent(parent)
|
|
714
|
+
if cursor is None:
|
|
715
|
+
position = _lazy_space(self._text, parent._start + 1)
|
|
716
|
+
item_index = 0
|
|
717
|
+
else:
|
|
718
|
+
self._check_cursor(parent, cursor)
|
|
719
|
+
position = cursor._position
|
|
720
|
+
item_index = cursor._item_index
|
|
721
|
+
_lazy_cancel(cancel_check)
|
|
722
|
+
closing = "}" if self._text[parent._start] == "{" else "]"
|
|
723
|
+
if position < parent._end and self._text[position] == closing:
|
|
724
|
+
return TrustedSnapshotPage(
|
|
725
|
+
parent,
|
|
726
|
+
cursor,
|
|
727
|
+
(),
|
|
728
|
+
None,
|
|
729
|
+
0,
|
|
730
|
+
"This snapshot container is empty.",
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
nodes: list[TrustedSnapshotNode] = []
|
|
734
|
+
rendered_text_bytes = 0
|
|
735
|
+
continuation: TrustedSnapshotContinuation | None = None
|
|
736
|
+
while position < parent._end:
|
|
737
|
+
# The unprocessed item proves a continuation is needed. Reserving
|
|
738
|
+
# one UI child keeps data rows + Load more at no more than 128.
|
|
739
|
+
if len(nodes) >= TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION:
|
|
740
|
+
continuation = self._next(parent, position, item_index)
|
|
741
|
+
break
|
|
742
|
+
if item_index % 4_096 == 0:
|
|
743
|
+
_lazy_cancel(cancel_check)
|
|
744
|
+
item_start = position
|
|
745
|
+
if closing == "}":
|
|
746
|
+
if self._text[position] != '"':
|
|
747
|
+
raise _SnapshotSyntaxError(
|
|
748
|
+
position, "An object key must be a string."
|
|
749
|
+
)
|
|
750
|
+
key_start = position
|
|
751
|
+
exact_key, key_end = _lazy_scan_string(self._text, position)
|
|
752
|
+
position = _lazy_space(self._text, key_end)
|
|
753
|
+
if position >= parent._end or self._text[position] != ":":
|
|
754
|
+
raise _SnapshotSyntaxError(position, "An object key needs a value.")
|
|
755
|
+
value_start = _lazy_space(self._text, position + 1)
|
|
756
|
+
else:
|
|
757
|
+
exact_key = f"[{item_index}]"
|
|
758
|
+
key_start = -1
|
|
759
|
+
key_end = -1
|
|
760
|
+
value_start = position
|
|
761
|
+
|
|
762
|
+
value_end = _lazy_skip_value(
|
|
763
|
+
self._text,
|
|
764
|
+
value_start,
|
|
765
|
+
depth=parent._depth,
|
|
766
|
+
cancel_check=cancel_check,
|
|
767
|
+
)
|
|
768
|
+
node = self._node(
|
|
769
|
+
exact_key,
|
|
770
|
+
key_start,
|
|
771
|
+
key_end,
|
|
772
|
+
value_start,
|
|
773
|
+
value_end,
|
|
774
|
+
child_depth=parent._depth + 1,
|
|
775
|
+
)
|
|
776
|
+
node_bytes = len(node.key.encode("utf-8")) + len(node.value.encode("utf-8"))
|
|
777
|
+
if nodes and (rendered_text_bytes + node_bytes > _PAGE_MAX_DATA_TEXT_BYTES):
|
|
778
|
+
continuation = self._next(parent, item_start, item_index)
|
|
779
|
+
break
|
|
780
|
+
nodes.append(node)
|
|
781
|
+
rendered_text_bytes += node_bytes
|
|
782
|
+
|
|
783
|
+
position = _lazy_space(self._text, value_end)
|
|
784
|
+
item_index += 1
|
|
785
|
+
if position >= parent._end:
|
|
786
|
+
raise _SnapshotSyntaxError(position, "A container is not closed.")
|
|
787
|
+
if self._text[position] == closing:
|
|
788
|
+
break
|
|
789
|
+
if self._text[position] != ",":
|
|
790
|
+
raise _SnapshotSyntaxError(position, "Container items need a comma.")
|
|
791
|
+
position = _lazy_space(self._text, position + 1)
|
|
792
|
+
|
|
793
|
+
_lazy_cancel(cancel_check)
|
|
794
|
+
return TrustedSnapshotPage(
|
|
795
|
+
parent,
|
|
796
|
+
cursor,
|
|
797
|
+
tuple(nodes),
|
|
798
|
+
continuation,
|
|
799
|
+
rendered_text_bytes,
|
|
800
|
+
(
|
|
801
|
+
"More children are available on demand."
|
|
802
|
+
if continuation is not None
|
|
803
|
+
else "All children in this snapshot container are loaded."
|
|
804
|
+
),
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
def _node(
|
|
808
|
+
self,
|
|
809
|
+
exact_key: str,
|
|
810
|
+
key_start: int,
|
|
811
|
+
key_end: int,
|
|
812
|
+
value_start: int,
|
|
813
|
+
value_end: int,
|
|
814
|
+
*,
|
|
815
|
+
child_depth: int,
|
|
816
|
+
) -> TrustedSnapshotNode:
|
|
817
|
+
key_utf8_bytes = len(exact_key.encode("utf-8"))
|
|
818
|
+
key, key_shortened = _truncate_utf8(
|
|
819
|
+
exact_key, TRUSTED_SNAPSHOT_MAX_NODE_KEY_BYTES
|
|
820
|
+
)
|
|
821
|
+
full_key = (
|
|
822
|
+
self._full(key_start, key_end, "string")
|
|
823
|
+
if key_shortened and key_start >= 0
|
|
824
|
+
else None
|
|
825
|
+
)
|
|
826
|
+
if self._text[value_start] in "{[":
|
|
827
|
+
value = ""
|
|
828
|
+
value_utf8_bytes = 0
|
|
829
|
+
source_value_bytes = len(self._text[value_start:value_end].encode("utf-8"))
|
|
830
|
+
value_shortened = False
|
|
831
|
+
full_value = None
|
|
832
|
+
container = self._container(
|
|
833
|
+
value_start,
|
|
834
|
+
value_end,
|
|
835
|
+
depth=child_depth,
|
|
836
|
+
)
|
|
837
|
+
else:
|
|
838
|
+
scalar = _lazy_scalar(self._text, value_start)
|
|
839
|
+
if scalar.end != value_end:
|
|
840
|
+
raise _SnapshotSyntaxError(value_start, "A scalar is invalid.")
|
|
841
|
+
value_utf8_bytes = len(scalar.display.encode("utf-8"))
|
|
842
|
+
source_value_bytes = value_utf8_bytes
|
|
843
|
+
value, value_shortened = _snapshot_value_preview(scalar.display)
|
|
844
|
+
full_value = (
|
|
845
|
+
self._full(scalar.start, scalar.end, scalar.kind)
|
|
846
|
+
if value_shortened
|
|
847
|
+
else None
|
|
848
|
+
)
|
|
849
|
+
container = None
|
|
850
|
+
tooltip_parts = []
|
|
851
|
+
if container is not None:
|
|
852
|
+
tooltip_parts.append("Expand to load this container's children on demand.")
|
|
853
|
+
elif value:
|
|
854
|
+
tooltip_parts.append(value)
|
|
855
|
+
if key_shortened:
|
|
856
|
+
tooltip_parts.append(
|
|
857
|
+
f"Key shortened from {key_utf8_bytes} UTF-8 bytes; "
|
|
858
|
+
"exact text is available on demand."
|
|
859
|
+
)
|
|
860
|
+
if value_shortened:
|
|
861
|
+
tooltip_parts.append(
|
|
862
|
+
f"Value shortened from {value_utf8_bytes} UTF-8 bytes; "
|
|
863
|
+
"exact text is available on demand."
|
|
864
|
+
)
|
|
865
|
+
tooltip = _truncate_utf8(
|
|
866
|
+
"\n".join(tooltip_parts), TRUSTED_SNAPSHOT_MAX_TOOLTIP_BYTES
|
|
867
|
+
)[0]
|
|
868
|
+
return TrustedSnapshotNode(
|
|
869
|
+
key=key,
|
|
870
|
+
value=value,
|
|
871
|
+
tooltip=tooltip,
|
|
872
|
+
container_handle=container,
|
|
873
|
+
full_key_handle=full_key,
|
|
874
|
+
full_value_handle=full_value,
|
|
875
|
+
key_shortened=key_shortened,
|
|
876
|
+
value_shortened=value_shortened,
|
|
877
|
+
key_utf8_bytes=key_utf8_bytes,
|
|
878
|
+
value_utf8_bytes=value_utf8_bytes,
|
|
879
|
+
source_position=value_start,
|
|
880
|
+
source_key_bytes=key_utf8_bytes,
|
|
881
|
+
source_value_bytes=source_value_bytes,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
def root_scalar_node(self) -> TrustedSnapshotNode:
|
|
885
|
+
scalar = self._root_scalar
|
|
886
|
+
if scalar is None:
|
|
887
|
+
raise ValueError("This snapshot has a container root.")
|
|
888
|
+
value_utf8_bytes = len(scalar.display.encode("utf-8"))
|
|
889
|
+
value, shortened = _snapshot_value_preview(scalar.display)
|
|
890
|
+
full_value = (
|
|
891
|
+
self._full(scalar.start, scalar.end, scalar.kind) if shortened else None
|
|
892
|
+
)
|
|
893
|
+
tooltip = _truncate_utf8(
|
|
894
|
+
value
|
|
895
|
+
+ (
|
|
896
|
+
f"\nValue shortened from {value_utf8_bytes} UTF-8 bytes; "
|
|
897
|
+
"exact text is available on demand."
|
|
898
|
+
if shortened
|
|
899
|
+
else ""
|
|
900
|
+
),
|
|
901
|
+
TRUSTED_SNAPSHOT_MAX_TOOLTIP_BYTES,
|
|
902
|
+
)[0]
|
|
903
|
+
return TrustedSnapshotNode(
|
|
904
|
+
"Value",
|
|
905
|
+
value,
|
|
906
|
+
tooltip=tooltip,
|
|
907
|
+
full_value_handle=full_value,
|
|
908
|
+
value_shortened=shortened,
|
|
909
|
+
key_utf8_bytes=len(b"Value"),
|
|
910
|
+
value_utf8_bytes=value_utf8_bytes,
|
|
911
|
+
source_position=scalar.start,
|
|
912
|
+
source_key_bytes=len(b"Value"),
|
|
913
|
+
source_value_bytes=value_utf8_bytes,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
def full_value(
|
|
917
|
+
self,
|
|
918
|
+
handle: TrustedSnapshotFullValueHandle,
|
|
919
|
+
*,
|
|
920
|
+
cancel_check: Callable[[], None] | None = None,
|
|
921
|
+
) -> TrustedSnapshotFullValue:
|
|
922
|
+
"""Resolve one exact shortened key or scalar from the retained source."""
|
|
923
|
+
|
|
924
|
+
if not isinstance(handle, TrustedSnapshotFullValueHandle):
|
|
925
|
+
raise TypeError("handle must be a TrustedSnapshotFullValueHandle.")
|
|
926
|
+
expected = self._signature("full", handle._start, handle._end, handle._kind)
|
|
927
|
+
if (
|
|
928
|
+
handle._session != self.session
|
|
929
|
+
or not secrets.compare_digest(handle.token, expected)
|
|
930
|
+
or not 0 <= handle._start < handle._end <= len(self._text)
|
|
931
|
+
):
|
|
932
|
+
raise ValueError("The snapshot full-value handle is stale or foreign.")
|
|
933
|
+
_lazy_cancel(cancel_check)
|
|
934
|
+
if handle._kind == "string":
|
|
935
|
+
value, end = _lazy_scan_string(self._text, handle._start)
|
|
936
|
+
if end != handle._end:
|
|
937
|
+
raise ValueError("The snapshot full-value handle is invalid.")
|
|
938
|
+
else:
|
|
939
|
+
scalar = _lazy_scalar(self._text, handle._start)
|
|
940
|
+
if scalar.end != handle._end or scalar.kind != handle._kind:
|
|
941
|
+
raise ValueError("The snapshot full-value handle is invalid.")
|
|
942
|
+
value = scalar.display
|
|
943
|
+
_lazy_cancel(cancel_check)
|
|
944
|
+
return TrustedSnapshotFullValue(value, len(value.encode("utf-8")))
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _lazy_diagnostic(
|
|
948
|
+
key: str,
|
|
949
|
+
message: str,
|
|
950
|
+
*,
|
|
951
|
+
status: SnapshotStatus,
|
|
952
|
+
input_bytes: int,
|
|
953
|
+
) -> TrustedSnapshotView:
|
|
954
|
+
key_text = _truncate_utf8(key, TRUSTED_SNAPSHOT_MAX_NODE_KEY_BYTES)[0]
|
|
955
|
+
value_text = _truncate_utf8(message, TRUSTED_SNAPSHOT_MAX_NODE_VALUE_BYTES)[0]
|
|
956
|
+
node = TrustedSnapshotNode(
|
|
957
|
+
key_text,
|
|
958
|
+
value_text,
|
|
959
|
+
tooltip=_truncate_utf8(message, TRUSTED_SNAPSHOT_MAX_TOOLTIP_BYTES)[0],
|
|
960
|
+
key_utf8_bytes=len(key.encode("utf-8")),
|
|
961
|
+
value_utf8_bytes=len(message.encode("utf-8")),
|
|
962
|
+
source_position=0,
|
|
963
|
+
source_key_bytes=len(key.encode("utf-8")),
|
|
964
|
+
source_value_bytes=len(message.encode("utf-8")),
|
|
965
|
+
)
|
|
966
|
+
rendered = len(key_text.encode("utf-8")) + len(value_text.encode("utf-8"))
|
|
967
|
+
page = TrustedSnapshotPage(None, None, (node,), None, rendered, value_text)
|
|
968
|
+
return TrustedSnapshotView(
|
|
969
|
+
page,
|
|
970
|
+
(),
|
|
971
|
+
status,
|
|
972
|
+
value_text,
|
|
973
|
+
input_bytes,
|
|
974
|
+
"",
|
|
975
|
+
None,
|
|
976
|
+
None,
|
|
977
|
+
None,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def normalize_trusted_snapshot(
|
|
982
|
+
snapshot_json: object,
|
|
983
|
+
*,
|
|
984
|
+
omission: TrustedSnapshotOmission | None = None,
|
|
985
|
+
cancel_check: Callable[[], None] | None = None,
|
|
986
|
+
) -> TrustedSnapshotView:
|
|
987
|
+
"""Validate a bounded source and return only its initial root page."""
|
|
988
|
+
|
|
989
|
+
if omission is not None:
|
|
990
|
+
input_bytes = max(0, omission.input_bytes or 0)
|
|
991
|
+
if omission.kind == "payload_limit":
|
|
992
|
+
size = (
|
|
993
|
+
f"its {input_bytes}-byte payload"
|
|
994
|
+
if omission.input_bytes is not None
|
|
995
|
+
else "its payload"
|
|
996
|
+
)
|
|
997
|
+
message = (
|
|
998
|
+
f"A snapshot was stored, but {size} exceeds the snapshot viewing limit."
|
|
999
|
+
)
|
|
1000
|
+
elif omission.kind == "detail_budget":
|
|
1001
|
+
message = (
|
|
1002
|
+
"A snapshot was stored, but it could not be retained within the "
|
|
1003
|
+
"selected-detail viewing budget."
|
|
1004
|
+
)
|
|
1005
|
+
else:
|
|
1006
|
+
message = (
|
|
1007
|
+
"A snapshot was stored, but it changed while the bounded reader "
|
|
1008
|
+
"was fetching it."
|
|
1009
|
+
)
|
|
1010
|
+
return _lazy_diagnostic(
|
|
1011
|
+
"Snapshot unavailable",
|
|
1012
|
+
message,
|
|
1013
|
+
status="unavailable",
|
|
1014
|
+
input_bytes=input_bytes,
|
|
1015
|
+
)
|
|
1016
|
+
if snapshot_json is None:
|
|
1017
|
+
return _lazy_diagnostic(
|
|
1018
|
+
"Snapshot",
|
|
1019
|
+
"No snapshot was stored for this run.",
|
|
1020
|
+
status="empty",
|
|
1021
|
+
input_bytes=0,
|
|
1022
|
+
)
|
|
1023
|
+
if not isinstance(snapshot_json, str):
|
|
1024
|
+
return _lazy_diagnostic(
|
|
1025
|
+
"Snapshot unavailable",
|
|
1026
|
+
"The stored snapshot is not JSON text.",
|
|
1027
|
+
status="unavailable",
|
|
1028
|
+
input_bytes=0,
|
|
1029
|
+
)
|
|
1030
|
+
try:
|
|
1031
|
+
input_bytes = len(snapshot_json.encode("utf-8"))
|
|
1032
|
+
except UnicodeEncodeError:
|
|
1033
|
+
return _lazy_diagnostic(
|
|
1034
|
+
"Snapshot unavailable",
|
|
1035
|
+
"The stored snapshot contains invalid Unicode text.",
|
|
1036
|
+
status="malformed",
|
|
1037
|
+
input_bytes=0,
|
|
1038
|
+
)
|
|
1039
|
+
if input_bytes > TRUSTED_SNAPSHOT_MAX_INPUT_BYTES:
|
|
1040
|
+
return _lazy_diagnostic(
|
|
1041
|
+
"Snapshot unavailable",
|
|
1042
|
+
"Snapshot JSON exceeds the 4194304-byte decode limit.",
|
|
1043
|
+
status="unavailable",
|
|
1044
|
+
input_bytes=input_bytes,
|
|
1045
|
+
)
|
|
1046
|
+
try:
|
|
1047
|
+
validated = _LazySnapshotValidator(snapshot_json, cancel_check).validate()
|
|
1048
|
+
except _LazySnapshotLimit as error:
|
|
1049
|
+
return _lazy_diagnostic(
|
|
1050
|
+
_MARKER_KEY,
|
|
1051
|
+
str(error),
|
|
1052
|
+
status="truncated",
|
|
1053
|
+
input_bytes=input_bytes,
|
|
1054
|
+
)
|
|
1055
|
+
except (_SnapshotSyntaxError, ValueError) as error:
|
|
1056
|
+
return _lazy_diagnostic(
|
|
1057
|
+
"Snapshot unavailable",
|
|
1058
|
+
f"Malformed snapshot JSON near character {getattr(error, 'position', 0)}.",
|
|
1059
|
+
status="malformed",
|
|
1060
|
+
input_bytes=input_bytes,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
source = TrustedSnapshotSource._create(snapshot_json, input_bytes, validated)
|
|
1064
|
+
if validated.empty_container:
|
|
1065
|
+
root = cast(TrustedSnapshotContainerHandle, source.root)
|
|
1066
|
+
page = source.page(root, None, cancel_check=cancel_check)
|
|
1067
|
+
return TrustedSnapshotView(
|
|
1068
|
+
page,
|
|
1069
|
+
source.parameters,
|
|
1070
|
+
"empty",
|
|
1071
|
+
"The snapshot contains an empty JSON container.",
|
|
1072
|
+
input_bytes,
|
|
1073
|
+
source.session,
|
|
1074
|
+
root,
|
|
1075
|
+
None,
|
|
1076
|
+
source,
|
|
1077
|
+
)
|
|
1078
|
+
if source.root is None:
|
|
1079
|
+
node = source.root_scalar_node()
|
|
1080
|
+
rendered = len(node.key.encode("utf-8")) + len(node.value.encode("utf-8"))
|
|
1081
|
+
page = TrustedSnapshotPage(
|
|
1082
|
+
None,
|
|
1083
|
+
None,
|
|
1084
|
+
(node,),
|
|
1085
|
+
None,
|
|
1086
|
+
rendered,
|
|
1087
|
+
"The scalar snapshot root is loaded.",
|
|
1088
|
+
)
|
|
1089
|
+
else:
|
|
1090
|
+
page = source.page(source.root, None, cancel_check=cancel_check)
|
|
1091
|
+
message = "Snapshot validated; nested content is loaded on demand."
|
|
1092
|
+
return TrustedSnapshotView(
|
|
1093
|
+
page,
|
|
1094
|
+
source.parameters,
|
|
1095
|
+
"available",
|
|
1096
|
+
message,
|
|
1097
|
+
input_bytes,
|
|
1098
|
+
source.session,
|
|
1099
|
+
source.root,
|
|
1100
|
+
page.continuation,
|
|
1101
|
+
source,
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
__all__ = [
|
|
1106
|
+
"TRUSTED_SNAPSHOT_MAX_CONTAINER_ITEMS",
|
|
1107
|
+
"TRUSTED_SNAPSHOT_MAX_DEPTH",
|
|
1108
|
+
"TRUSTED_SNAPSHOT_MAX_INPUT_BYTES",
|
|
1109
|
+
"TRUSTED_SNAPSHOT_MAX_NODE_KEY_BYTES",
|
|
1110
|
+
"TRUSTED_SNAPSHOT_MAX_NODE_VALUE_BYTES",
|
|
1111
|
+
"TRUSTED_SNAPSHOT_MAX_PARAMETER_VALUE_BYTES",
|
|
1112
|
+
"TRUSTED_SNAPSHOT_MAX_PARAMETER_VIEWS",
|
|
1113
|
+
"TRUSTED_SNAPSHOT_MAX_RENDERED_NODES",
|
|
1114
|
+
"TRUSTED_SNAPSHOT_MAX_RENDERED_TEXT_BYTES",
|
|
1115
|
+
"TRUSTED_SNAPSHOT_MAX_SCALAR_BYTES",
|
|
1116
|
+
"TRUSTED_SNAPSHOT_MAX_TOOLTIP_BYTES",
|
|
1117
|
+
"TRUSTED_SNAPSHOT_LOAD_MORE_TEXT",
|
|
1118
|
+
"TRUSTED_SNAPSHOT_PAGE_MAX_CHILDREN",
|
|
1119
|
+
"TRUSTED_SNAPSHOT_PAGE_MAX_DATA_NODES_WITH_CONTINUATION",
|
|
1120
|
+
"TRUSTED_SNAPSHOT_PAGE_MAX_TEXT_BYTES",
|
|
1121
|
+
"TrustedSnapshotContainerHandle",
|
|
1122
|
+
"TrustedSnapshotContinuation",
|
|
1123
|
+
"TrustedSnapshotFullValue",
|
|
1124
|
+
"TrustedSnapshotFullValueHandle",
|
|
1125
|
+
"TrustedSnapshotNode",
|
|
1126
|
+
"TrustedSnapshotOmission",
|
|
1127
|
+
"TrustedSnapshotPage",
|
|
1128
|
+
"TrustedSnapshotParameterView",
|
|
1129
|
+
"TrustedSnapshotSource",
|
|
1130
|
+
"TrustedSnapshotView",
|
|
1131
|
+
"normalize_trusted_snapshot",
|
|
1132
|
+
]
|