qplotter 1.6.0__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 (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. qplotter-1.6.0.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
+ ]