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,1065 @@
|
|
|
1
|
+
"""Bounded selected-detail presentation models created before Qt publication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Literal, TypeAlias
|
|
9
|
+
|
|
10
|
+
TRUSTED_PRESENTATION_MAX_DEPTH = 16
|
|
11
|
+
TRUSTED_PRESENTATION_MAX_CONTAINER_ITEMS = 2_048
|
|
12
|
+
TRUSTED_PRESENTATION_MAX_RENDERED_NODES = 512
|
|
13
|
+
TRUSTED_PRESENTATION_MAX_RENDERED_TEXT_BYTES = 64 * 1024
|
|
14
|
+
TRUSTED_PRESENTATION_MAX_TOOLTIP_TEXT_BYTES = 64 * 1024
|
|
15
|
+
TRUSTED_PRESENTATION_MAX_KEY_BYTES = 256
|
|
16
|
+
TRUSTED_PRESENTATION_MAX_VALUE_BYTES = 256
|
|
17
|
+
TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES = 1_024
|
|
18
|
+
TRUSTED_PRESENTATION_MAX_RUN_FIELDS = 64
|
|
19
|
+
TRUSTED_PRESENTATION_MAX_METADATA_FIELDS = 64
|
|
20
|
+
TRUSTED_PRESENTATION_MAX_SEQUENCE_ITEMS = 32
|
|
21
|
+
TRUSTED_PRESENTATION_MAX_FIELD_VALUE_BYTES = 512
|
|
22
|
+
TRUSTED_PRESENTATION_MAX_COMPATIBILITY_TEXT_BYTES = 64 * 1024
|
|
23
|
+
TRUSTED_PRESENTATION_MAX_PARAMETERS = 256
|
|
24
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_TOTAL_TEXT_BYTES = 128 * 1024
|
|
25
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_TEXT_BYTES = 256
|
|
26
|
+
TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES = 32
|
|
27
|
+
TRUSTED_PRESENTATION_MAX_UNAVAILABLE_FIELDS = 256
|
|
28
|
+
TRUSTED_PRESENTATION_MAX_ERROR_BYTES = 1_024
|
|
29
|
+
TRUSTED_PRESENTATION_MAX_FULL_VALUE_TEXT_BYTES = 4 * 1024 * 1024
|
|
30
|
+
TRUSTED_PRESENTATION_MAX_PATH_BYTES = 1_024
|
|
31
|
+
|
|
32
|
+
_FINAL_ROW_COUNT = 2
|
|
33
|
+
_TEXT_RESERVE_BYTES = _FINAL_ROW_COUNT * (
|
|
34
|
+
TRUSTED_PRESENTATION_MAX_KEY_BYTES + TRUSTED_PRESENTATION_MAX_VALUE_BYTES
|
|
35
|
+
)
|
|
36
|
+
_TOOLTIP_RESERVE_BYTES = _FINAL_ROW_COUNT * TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES
|
|
37
|
+
_TRUNCATION_KEY = "[truncated]"
|
|
38
|
+
_DISPLAY_KEY = "[display]"
|
|
39
|
+
_VIEW_FULL_SUFFIX = " ... [view full]"
|
|
40
|
+
_VIEW_FULL_KEY_SUFFIX = " ... [view full key]"
|
|
41
|
+
_RUN_TRUNCATION_FIELD = (
|
|
42
|
+
"presentation_status",
|
|
43
|
+
"Some run fields were truncated; see Raw tab.",
|
|
44
|
+
)
|
|
45
|
+
_METADATA_TRUNCATION_VALUE = "Additional or oversized metadata is shown as truncated."
|
|
46
|
+
|
|
47
|
+
PresentationScalar: TypeAlias = None | bool | int | float | str
|
|
48
|
+
PresentationValue: TypeAlias = PresentationScalar | tuple[PresentationScalar, ...]
|
|
49
|
+
FrozenPresentationFields: TypeAlias = tuple[tuple[str, PresentationValue], ...]
|
|
50
|
+
PresentationStatus: TypeAlias = Literal["available", "empty", "truncated"]
|
|
51
|
+
|
|
52
|
+
_SELECTED_RUN_FIELDS = (
|
|
53
|
+
"run_id",
|
|
54
|
+
"exp_id",
|
|
55
|
+
"name",
|
|
56
|
+
"result_table_name",
|
|
57
|
+
"result_counter",
|
|
58
|
+
"run_timestamp",
|
|
59
|
+
"completed_timestamp",
|
|
60
|
+
"is_completed",
|
|
61
|
+
"guid",
|
|
62
|
+
"captured_run_id",
|
|
63
|
+
"captured_counter",
|
|
64
|
+
"database_modified_timestamp",
|
|
65
|
+
"expected_results",
|
|
66
|
+
"expected_results_source",
|
|
67
|
+
"measure_parameters",
|
|
68
|
+
"preview_dimensions",
|
|
69
|
+
"measurement_exception",
|
|
70
|
+
"parameters_truncated",
|
|
71
|
+
"point_shape",
|
|
72
|
+
"read_setpoint_count",
|
|
73
|
+
"result_count",
|
|
74
|
+
"setpoint_count",
|
|
75
|
+
"setpoint_count_source",
|
|
76
|
+
"setpoint_shape",
|
|
77
|
+
"setpoint_shape_source",
|
|
78
|
+
"storage_bytes",
|
|
79
|
+
"storage_bytes_estimated",
|
|
80
|
+
"sweep_parameters",
|
|
81
|
+
"exp_name",
|
|
82
|
+
"sample_name",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class TrustedPresentationNode:
|
|
88
|
+
"""One bounded tree row with a separately bounded tooltip."""
|
|
89
|
+
|
|
90
|
+
key: str
|
|
91
|
+
value: str
|
|
92
|
+
tooltip: str
|
|
93
|
+
parent_index: int | None
|
|
94
|
+
path: str = ""
|
|
95
|
+
key_shortened: bool = False
|
|
96
|
+
value_shortened: bool = False
|
|
97
|
+
full_key_id: str | None = None
|
|
98
|
+
full_value_id: str | None = None
|
|
99
|
+
source_key_bytes: int = 0
|
|
100
|
+
source_value_bytes: int = 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class TrustedPresentationFullValue:
|
|
105
|
+
"""One exact scalar retained once for the current bounded detail."""
|
|
106
|
+
|
|
107
|
+
identifier: str
|
|
108
|
+
text: str = field(repr=False)
|
|
109
|
+
utf8_bytes: int
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True, slots=True)
|
|
113
|
+
class TrustedPresentationView:
|
|
114
|
+
"""Immutable flat tree safe for iterative construction by Qt."""
|
|
115
|
+
|
|
116
|
+
nodes: tuple[TrustedPresentationNode, ...]
|
|
117
|
+
status: PresentationStatus
|
|
118
|
+
message: str
|
|
119
|
+
inspected_items: int
|
|
120
|
+
rendered_text_bytes: int
|
|
121
|
+
tooltip_text_bytes: int
|
|
122
|
+
shortened_key_count: int = 0
|
|
123
|
+
shortened_value_count: int = 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class TrustedSelectedRunPresentation:
|
|
128
|
+
"""All non-snapshot selected-detail values that may cross into Qt."""
|
|
129
|
+
|
|
130
|
+
run_fields: FrozenPresentationFields
|
|
131
|
+
metadata_fields: FrozenPresentationFields
|
|
132
|
+
metadata: TrustedPresentationView
|
|
133
|
+
raw: TrustedPresentationView
|
|
134
|
+
parameters_truncated: bool = False
|
|
135
|
+
full_values: tuple[TrustedPresentationFullValue, ...] = field(
|
|
136
|
+
default=(),
|
|
137
|
+
repr=False,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(slots=True)
|
|
142
|
+
class _NodeBuilder:
|
|
143
|
+
key: str
|
|
144
|
+
value: str
|
|
145
|
+
tooltip: str
|
|
146
|
+
parent_index: int | None
|
|
147
|
+
path: str = ""
|
|
148
|
+
key_shortened: bool = False
|
|
149
|
+
value_shortened: bool = False
|
|
150
|
+
full_key_id: str | None = None
|
|
151
|
+
full_value_id: str | None = None
|
|
152
|
+
source_key_bytes: int = 0
|
|
153
|
+
source_value_bytes: int = 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True, slots=True)
|
|
157
|
+
class _Task:
|
|
158
|
+
key: object
|
|
159
|
+
value: object
|
|
160
|
+
parent_index: int | None
|
|
161
|
+
depth: int
|
|
162
|
+
root: bool = False
|
|
163
|
+
path: tuple[str, ...] = ()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class _ExitContainer:
|
|
168
|
+
container_id: int
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class _FullValueRegistry:
|
|
172
|
+
"""Deduplicate exact selected-detail strings under one aggregate bound."""
|
|
173
|
+
|
|
174
|
+
def __init__(self) -> None:
|
|
175
|
+
self._values_by_identity: dict[int, TrustedPresentationFullValue] = {}
|
|
176
|
+
self._values: list[TrustedPresentationFullValue] = []
|
|
177
|
+
self.text_bytes = 0
|
|
178
|
+
|
|
179
|
+
def retain(self, text: str) -> TrustedPresentationFullValue | None:
|
|
180
|
+
retained = self._values_by_identity.get(id(text))
|
|
181
|
+
if retained is not None and retained.text is text:
|
|
182
|
+
return retained
|
|
183
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
184
|
+
if (
|
|
185
|
+
len(encoded) > TRUSTED_PRESENTATION_MAX_FULL_VALUE_TEXT_BYTES
|
|
186
|
+
or self.text_bytes + len(encoded)
|
|
187
|
+
> TRUSTED_PRESENTATION_MAX_FULL_VALUE_TEXT_BYTES
|
|
188
|
+
):
|
|
189
|
+
return None
|
|
190
|
+
identifier = f"value:{len(self._values)}"
|
|
191
|
+
retained = TrustedPresentationFullValue(identifier, text, len(encoded))
|
|
192
|
+
self._values_by_identity[id(text)] = retained
|
|
193
|
+
self._values.append(retained)
|
|
194
|
+
self.text_bytes += len(encoded)
|
|
195
|
+
return retained
|
|
196
|
+
|
|
197
|
+
def checkpoint(self) -> int:
|
|
198
|
+
return len(self._values)
|
|
199
|
+
|
|
200
|
+
def rollback(self, checkpoint: int) -> None:
|
|
201
|
+
while len(self._values) > checkpoint:
|
|
202
|
+
retained = self._values.pop()
|
|
203
|
+
if self._values_by_identity.get(id(retained.text)) is retained:
|
|
204
|
+
del self._values_by_identity[id(retained.text)]
|
|
205
|
+
self.text_bytes -= retained.utf8_bytes
|
|
206
|
+
|
|
207
|
+
def freeze(self) -> tuple[TrustedPresentationFullValue, ...]:
|
|
208
|
+
return tuple(self._values)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class _PresentationNormalizer:
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
value: object,
|
|
215
|
+
*,
|
|
216
|
+
full_values: _FullValueRegistry | None = None,
|
|
217
|
+
path_prefix: str = "",
|
|
218
|
+
initial_reasons: Sequence[str] = (),
|
|
219
|
+
) -> None:
|
|
220
|
+
self.value = value
|
|
221
|
+
self.full_values = full_values
|
|
222
|
+
self.path_prefix = _bounded_path_prefix(path_prefix)
|
|
223
|
+
self.nodes: list[_NodeBuilder] = []
|
|
224
|
+
self.inspected_items = 0
|
|
225
|
+
self.rendered_text_bytes = 0
|
|
226
|
+
self.tooltip_text_bytes = 0
|
|
227
|
+
self.reasons: list[str] = list(dict.fromkeys(initial_reasons))
|
|
228
|
+
self.shortened_key_count = 0
|
|
229
|
+
self.shortened_value_count = 0
|
|
230
|
+
self.ancestor_containers: set[int] = set()
|
|
231
|
+
self.halted = False
|
|
232
|
+
|
|
233
|
+
def normalize(self) -> TrustedPresentationView:
|
|
234
|
+
tasks: list[_Task | _ExitContainer] = [
|
|
235
|
+
_Task("Value", self.value, None, 0, True, ())
|
|
236
|
+
]
|
|
237
|
+
while tasks and not self.halted:
|
|
238
|
+
if (
|
|
239
|
+
len(self.nodes)
|
|
240
|
+
>= TRUSTED_PRESENTATION_MAX_RENDERED_NODES - _FINAL_ROW_COUNT
|
|
241
|
+
):
|
|
242
|
+
self._reason("The 512-node rendering limit was reached.")
|
|
243
|
+
break
|
|
244
|
+
task = tasks.pop()
|
|
245
|
+
if isinstance(task, _ExitContainer):
|
|
246
|
+
self.ancestor_containers.discard(task.container_id)
|
|
247
|
+
continue
|
|
248
|
+
if _is_container(task.value):
|
|
249
|
+
self._schedule_container(task, tasks)
|
|
250
|
+
else:
|
|
251
|
+
self._add_scalar(task)
|
|
252
|
+
|
|
253
|
+
if tasks:
|
|
254
|
+
self._reason("Additional values were omitted from the bounded view.")
|
|
255
|
+
if self.shortened_key_count or self.shortened_value_count:
|
|
256
|
+
self._append_display_summary()
|
|
257
|
+
if self.reasons:
|
|
258
|
+
self._append_marker()
|
|
259
|
+
status: PresentationStatus = "truncated"
|
|
260
|
+
message = " ".join(dict.fromkeys(self.reasons))
|
|
261
|
+
elif self.nodes:
|
|
262
|
+
status = "available"
|
|
263
|
+
if self.shortened_key_count or self.shortened_value_count:
|
|
264
|
+
message = self._display_summary_text()
|
|
265
|
+
else:
|
|
266
|
+
message = "Selected detail normalized within all presentation limits."
|
|
267
|
+
else:
|
|
268
|
+
self._add_node("No data", "", "", None)
|
|
269
|
+
status = "empty"
|
|
270
|
+
message = "No selected-detail values were available."
|
|
271
|
+
return TrustedPresentationView(
|
|
272
|
+
nodes=tuple(
|
|
273
|
+
TrustedPresentationNode(
|
|
274
|
+
node.key,
|
|
275
|
+
node.value,
|
|
276
|
+
node.tooltip,
|
|
277
|
+
node.parent_index,
|
|
278
|
+
node.path,
|
|
279
|
+
node.key_shortened,
|
|
280
|
+
node.value_shortened,
|
|
281
|
+
node.full_key_id,
|
|
282
|
+
node.full_value_id,
|
|
283
|
+
node.source_key_bytes,
|
|
284
|
+
node.source_value_bytes,
|
|
285
|
+
)
|
|
286
|
+
for node in self.nodes
|
|
287
|
+
),
|
|
288
|
+
status=status,
|
|
289
|
+
message=message,
|
|
290
|
+
inspected_items=self.inspected_items,
|
|
291
|
+
rendered_text_bytes=self.rendered_text_bytes,
|
|
292
|
+
tooltip_text_bytes=self.tooltip_text_bytes,
|
|
293
|
+
shortened_key_count=self.shortened_key_count,
|
|
294
|
+
shortened_value_count=self.shortened_value_count,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def _schedule_container(
|
|
298
|
+
self,
|
|
299
|
+
task: _Task,
|
|
300
|
+
tasks: list[_Task | _ExitContainer],
|
|
301
|
+
) -> None:
|
|
302
|
+
container_id = id(task.value)
|
|
303
|
+
if container_id in self.ancestor_containers:
|
|
304
|
+
self._add_node(
|
|
305
|
+
task.key,
|
|
306
|
+
"[cyclic container unavailable]",
|
|
307
|
+
"[cyclic container unavailable]",
|
|
308
|
+
task.parent_index,
|
|
309
|
+
path=task.path,
|
|
310
|
+
)
|
|
311
|
+
self._reason("A cyclic container was omitted.")
|
|
312
|
+
return
|
|
313
|
+
self.ancestor_containers.add(container_id)
|
|
314
|
+
tasks.append(_ExitContainer(container_id))
|
|
315
|
+
|
|
316
|
+
parent_index = task.parent_index
|
|
317
|
+
if not task.root:
|
|
318
|
+
parent_index = self._add_node(
|
|
319
|
+
task.key,
|
|
320
|
+
"",
|
|
321
|
+
"",
|
|
322
|
+
task.parent_index,
|
|
323
|
+
path=task.path,
|
|
324
|
+
)
|
|
325
|
+
if parent_index is None:
|
|
326
|
+
return
|
|
327
|
+
if task.depth >= TRUSTED_PRESENTATION_MAX_DEPTH:
|
|
328
|
+
self._reason("The 16-level nesting limit was reached.")
|
|
329
|
+
self._add_node(
|
|
330
|
+
_TRUNCATION_KEY,
|
|
331
|
+
"Nested values omitted.",
|
|
332
|
+
"Nested values omitted.",
|
|
333
|
+
parent_index,
|
|
334
|
+
path=(*task.path, _TRUNCATION_KEY),
|
|
335
|
+
)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
children, has_more = self._bounded_children(task.value)
|
|
339
|
+
if has_more:
|
|
340
|
+
self._reason("The 2048-item container inspection limit was reached.")
|
|
341
|
+
for key, value in reversed(children):
|
|
342
|
+
tasks.append(
|
|
343
|
+
_Task(
|
|
344
|
+
key=key,
|
|
345
|
+
value=value,
|
|
346
|
+
parent_index=parent_index,
|
|
347
|
+
depth=task.depth + 1,
|
|
348
|
+
path=(*task.path, _path_segment(key)),
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def _bounded_children(
|
|
353
|
+
self, value: object
|
|
354
|
+
) -> tuple[list[tuple[object, object]], bool]:
|
|
355
|
+
remaining = TRUSTED_PRESENTATION_MAX_CONTAINER_ITEMS - self.inspected_items
|
|
356
|
+
if remaining <= 0:
|
|
357
|
+
return [], True
|
|
358
|
+
if isinstance(value, Mapping):
|
|
359
|
+
iterator = iter(value.items())
|
|
360
|
+
else:
|
|
361
|
+
iterator = enumerate(value) # type: ignore[arg-type]
|
|
362
|
+
|
|
363
|
+
children: list[tuple[object, object]] = []
|
|
364
|
+
for _index in range(remaining + 1):
|
|
365
|
+
try:
|
|
366
|
+
child = next(iterator)
|
|
367
|
+
except StopIteration:
|
|
368
|
+
return children, False
|
|
369
|
+
if len(children) >= remaining:
|
|
370
|
+
return children, True
|
|
371
|
+
children.append(child)
|
|
372
|
+
self.inspected_items += 1
|
|
373
|
+
return children, False
|
|
374
|
+
|
|
375
|
+
def _add_scalar(self, task: _Task) -> None:
|
|
376
|
+
text = _scalar_text(task.value)
|
|
377
|
+
node_key = "Value" if task.root else task.key
|
|
378
|
+
full_value = task.value if isinstance(task.value, str) else None
|
|
379
|
+
if isinstance(task.value, bytes) or not (
|
|
380
|
+
task.value is None or isinstance(task.value, (bool, int, float, str))
|
|
381
|
+
):
|
|
382
|
+
self._reason("A binary or unsupported scalar value was unavailable.")
|
|
383
|
+
self._add_node(
|
|
384
|
+
node_key,
|
|
385
|
+
text,
|
|
386
|
+
text,
|
|
387
|
+
task.parent_index,
|
|
388
|
+
path=task.path or (_path_segment(node_key),),
|
|
389
|
+
full_value=full_value,
|
|
390
|
+
head_and_tail=_key_text(node_key).casefold()
|
|
391
|
+
in {"measurement_exception", "exception", "traceback"},
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def _add_node(
|
|
395
|
+
self,
|
|
396
|
+
key: object,
|
|
397
|
+
value: str,
|
|
398
|
+
tooltip: str,
|
|
399
|
+
parent_index: int | None,
|
|
400
|
+
*,
|
|
401
|
+
path: tuple[str, ...] = (),
|
|
402
|
+
full_value: str | None = None,
|
|
403
|
+
head_and_tail: bool = False,
|
|
404
|
+
) -> int | None:
|
|
405
|
+
if (
|
|
406
|
+
len(self.nodes)
|
|
407
|
+
>= TRUSTED_PRESENTATION_MAX_RENDERED_NODES - _FINAL_ROW_COUNT
|
|
408
|
+
):
|
|
409
|
+
self._reason("The 512-node rendering limit was reached.")
|
|
410
|
+
self.halted = True
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
source_key = _key_text(key)
|
|
414
|
+
source_value = full_value if full_value is not None else value
|
|
415
|
+
source_key_bytes = len(source_key.encode("utf-8", errors="replace"))
|
|
416
|
+
source_value_bytes = len(source_value.encode("utf-8", errors="replace"))
|
|
417
|
+
backing_checkpoint = (
|
|
418
|
+
self.full_values.checkpoint() if self.full_values is not None else 0
|
|
419
|
+
)
|
|
420
|
+
key_shortened = source_key_bytes > TRUSTED_PRESENTATION_MAX_KEY_BYTES
|
|
421
|
+
value_shortened = bool(
|
|
422
|
+
full_value is not None
|
|
423
|
+
and (
|
|
424
|
+
source_value != value
|
|
425
|
+
or len(value.encode("utf-8", errors="replace"))
|
|
426
|
+
> TRUSTED_PRESENTATION_MAX_VALUE_BYTES
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
full_key_id = None
|
|
430
|
+
full_value_id = None
|
|
431
|
+
if key_shortened:
|
|
432
|
+
retained_key = self._retain_full_text(source_key)
|
|
433
|
+
if retained_key is not None:
|
|
434
|
+
full_key_id = retained_key.identifier
|
|
435
|
+
else:
|
|
436
|
+
self._reason(
|
|
437
|
+
"A complete scalar key was omitted at the 4194304-byte "
|
|
438
|
+
"selected-detail backing limit."
|
|
439
|
+
)
|
|
440
|
+
if value_shortened:
|
|
441
|
+
retained_value = self._retain_full_text(source_value)
|
|
442
|
+
if retained_value is not None:
|
|
443
|
+
full_value_id = retained_value.identifier
|
|
444
|
+
else:
|
|
445
|
+
self._reason(
|
|
446
|
+
"A complete scalar value was omitted at the 4194304-byte "
|
|
447
|
+
"selected-detail backing limit."
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if key_shortened:
|
|
451
|
+
key_text = _shortened_key_text(
|
|
452
|
+
source_key,
|
|
453
|
+
TRUSTED_PRESENTATION_MAX_KEY_BYTES,
|
|
454
|
+
available=full_key_id is not None,
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
key_text = source_key
|
|
458
|
+
if value_shortened:
|
|
459
|
+
value_text = _shortened_value_text(
|
|
460
|
+
value,
|
|
461
|
+
TRUSTED_PRESENTATION_MAX_VALUE_BYTES,
|
|
462
|
+
head_and_tail=head_and_tail,
|
|
463
|
+
available=full_value_id is not None,
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
value_text, _value_cell_truncated = _truncate_utf8(
|
|
467
|
+
value, TRUSTED_PRESENTATION_MAX_VALUE_BYTES
|
|
468
|
+
)
|
|
469
|
+
if key_shortened or value_shortened:
|
|
470
|
+
parts = []
|
|
471
|
+
actions = []
|
|
472
|
+
if key_shortened:
|
|
473
|
+
parts.append(f"key: {source_key_bytes} UTF-8 bytes")
|
|
474
|
+
if full_key_id is not None:
|
|
475
|
+
actions.append(
|
|
476
|
+
"Activate the marked key cell to view or explicitly copy "
|
|
477
|
+
"the complete key."
|
|
478
|
+
)
|
|
479
|
+
else:
|
|
480
|
+
actions.append(
|
|
481
|
+
"The complete key was not retained at the bounded detail limit."
|
|
482
|
+
)
|
|
483
|
+
if value_shortened:
|
|
484
|
+
parts.append(f"value: {source_value_bytes} UTF-8 bytes")
|
|
485
|
+
if full_value_id is not None:
|
|
486
|
+
actions.append(
|
|
487
|
+
"Activate the marked value cell to view or explicitly copy "
|
|
488
|
+
"the complete value."
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
actions.append(
|
|
492
|
+
"The complete value was not retained at the bounded detail "
|
|
493
|
+
"limit."
|
|
494
|
+
)
|
|
495
|
+
tooltip_text, _tooltip_truncated = _truncate_utf8(
|
|
496
|
+
f"Shortened for display ({'; '.join(parts)}). {' '.join(actions)}",
|
|
497
|
+
TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
tooltip_text, _tooltip_truncated = _truncate_utf8(
|
|
501
|
+
tooltip, TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
rendered_remaining = (
|
|
505
|
+
TRUSTED_PRESENTATION_MAX_RENDERED_TEXT_BYTES
|
|
506
|
+
- _TEXT_RESERVE_BYTES
|
|
507
|
+
- self.rendered_text_bytes
|
|
508
|
+
)
|
|
509
|
+
tooltip_remaining = (
|
|
510
|
+
TRUSTED_PRESENTATION_MAX_TOOLTIP_TEXT_BYTES
|
|
511
|
+
- _TOOLTIP_RESERVE_BYTES
|
|
512
|
+
- self.tooltip_text_bytes
|
|
513
|
+
)
|
|
514
|
+
key_bytes = len(key_text.encode("utf-8"))
|
|
515
|
+
value_bytes = len(value_text.encode("utf-8"))
|
|
516
|
+
tooltip_bytes = len(tooltip_text.encode("utf-8"))
|
|
517
|
+
if (
|
|
518
|
+
rendered_remaining < key_bytes + value_bytes
|
|
519
|
+
or tooltip_remaining < tooltip_bytes
|
|
520
|
+
):
|
|
521
|
+
if self.full_values is not None:
|
|
522
|
+
self.full_values.rollback(backing_checkpoint)
|
|
523
|
+
self._reason("The 65536-byte presentation text limit was reached.")
|
|
524
|
+
self.halted = True
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
node_index = len(self.nodes)
|
|
528
|
+
self.nodes.append(
|
|
529
|
+
_NodeBuilder(
|
|
530
|
+
key_text,
|
|
531
|
+
value_text,
|
|
532
|
+
tooltip_text,
|
|
533
|
+
parent_index,
|
|
534
|
+
_bounded_path(self.path_prefix, path),
|
|
535
|
+
key_shortened,
|
|
536
|
+
value_shortened,
|
|
537
|
+
full_key_id,
|
|
538
|
+
full_value_id,
|
|
539
|
+
source_key_bytes,
|
|
540
|
+
source_value_bytes,
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
self.rendered_text_bytes += key_bytes + value_bytes
|
|
544
|
+
self.tooltip_text_bytes += tooltip_bytes
|
|
545
|
+
self.shortened_key_count += int(key_shortened)
|
|
546
|
+
self.shortened_value_count += int(value_shortened)
|
|
547
|
+
return node_index
|
|
548
|
+
|
|
549
|
+
def _retain_full_text(self, text: str) -> TrustedPresentationFullValue | None:
|
|
550
|
+
if self.full_values is None:
|
|
551
|
+
return None
|
|
552
|
+
return self.full_values.retain(text)
|
|
553
|
+
|
|
554
|
+
def _display_summary_text(self) -> str:
|
|
555
|
+
parts = []
|
|
556
|
+
if self.shortened_key_count:
|
|
557
|
+
noun = "key" if self.shortened_key_count == 1 else "keys"
|
|
558
|
+
parts.append(f"{self.shortened_key_count} {noun}")
|
|
559
|
+
if self.shortened_value_count:
|
|
560
|
+
noun = "value" if self.shortened_value_count == 1 else "values"
|
|
561
|
+
parts.append(f"{self.shortened_value_count} {noun}")
|
|
562
|
+
subject = " and ".join(parts)
|
|
563
|
+
return (
|
|
564
|
+
f"{subject} shortened for display. Marked rows identify the affected "
|
|
565
|
+
"text; activate rows labelled 'view full' to view or explicitly copy "
|
|
566
|
+
"the complete text."
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def _append_display_summary(self) -> None:
|
|
570
|
+
if len(self.nodes) >= TRUSTED_PRESENTATION_MAX_RENDERED_NODES:
|
|
571
|
+
return
|
|
572
|
+
message, _ = _truncate_utf8(
|
|
573
|
+
self._display_summary_text(),
|
|
574
|
+
TRUSTED_PRESENTATION_MAX_VALUE_BYTES,
|
|
575
|
+
)
|
|
576
|
+
tooltip, _ = _truncate_utf8(message, _TOOLTIP_RESERVE_BYTES)
|
|
577
|
+
self.nodes.append(
|
|
578
|
+
_NodeBuilder(
|
|
579
|
+
_DISPLAY_KEY,
|
|
580
|
+
message,
|
|
581
|
+
tooltip,
|
|
582
|
+
None,
|
|
583
|
+
_bounded_path(self.path_prefix, (_DISPLAY_KEY,)),
|
|
584
|
+
)
|
|
585
|
+
)
|
|
586
|
+
self.rendered_text_bytes += len(_DISPLAY_KEY.encode("utf-8")) + len(
|
|
587
|
+
message.encode("utf-8")
|
|
588
|
+
)
|
|
589
|
+
self.tooltip_text_bytes += len(tooltip.encode("utf-8"))
|
|
590
|
+
|
|
591
|
+
def _append_marker(self) -> None:
|
|
592
|
+
if len(self.nodes) >= TRUSTED_PRESENTATION_MAX_RENDERED_NODES:
|
|
593
|
+
return
|
|
594
|
+
message = " ".join(dict.fromkeys(self.reasons))
|
|
595
|
+
message, _ = _truncate_utf8(
|
|
596
|
+
message,
|
|
597
|
+
TRUSTED_PRESENTATION_MAX_VALUE_BYTES,
|
|
598
|
+
)
|
|
599
|
+
tooltip, _ = _truncate_utf8(message, _TOOLTIP_RESERVE_BYTES)
|
|
600
|
+
self.nodes.append(
|
|
601
|
+
_NodeBuilder(
|
|
602
|
+
_TRUNCATION_KEY,
|
|
603
|
+
message,
|
|
604
|
+
tooltip,
|
|
605
|
+
None,
|
|
606
|
+
_bounded_path(self.path_prefix, (_TRUNCATION_KEY,)),
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
self.rendered_text_bytes += len(_TRUNCATION_KEY.encode("utf-8")) + len(
|
|
610
|
+
message.encode("utf-8")
|
|
611
|
+
)
|
|
612
|
+
self.tooltip_text_bytes += len(tooltip.encode("utf-8"))
|
|
613
|
+
|
|
614
|
+
def _reason(self, message: str) -> None:
|
|
615
|
+
if message not in self.reasons:
|
|
616
|
+
self.reasons.append(message)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _is_container(value: object) -> bool:
|
|
620
|
+
return isinstance(value, (Mapping, list, tuple))
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _key_text(value: object) -> str:
|
|
624
|
+
if isinstance(value, str):
|
|
625
|
+
return value
|
|
626
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
627
|
+
return str(value)
|
|
628
|
+
return f"<{type(value).__name__} key>"
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _scalar_text(value: object) -> str:
|
|
632
|
+
if value is None:
|
|
633
|
+
return ""
|
|
634
|
+
if isinstance(value, bytes):
|
|
635
|
+
return f"[binary value omitted: {len(value)} bytes]"
|
|
636
|
+
if isinstance(value, str):
|
|
637
|
+
return value.replace("\r", " ").replace("\n", " ")
|
|
638
|
+
if isinstance(value, float):
|
|
639
|
+
return f"{value:.6g}"
|
|
640
|
+
if isinstance(value, (bool, int)):
|
|
641
|
+
return str(value)
|
|
642
|
+
return f"[{type(value).__name__} value unavailable]"
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _bounded_path_prefix(value: str) -> str:
|
|
646
|
+
if not value:
|
|
647
|
+
return ""
|
|
648
|
+
return _bounded_path("", (_path_segment(value),))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _path_segment(value: object) -> str:
|
|
652
|
+
text = _key_text(value).replace("~", "~0").replace("/", "~1")
|
|
653
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
654
|
+
if len(encoded) <= TRUSTED_PRESENTATION_MAX_KEY_BYTES:
|
|
655
|
+
return text
|
|
656
|
+
digest = hashlib.sha256(encoded).hexdigest()[:16]
|
|
657
|
+
prefix, _ = _truncate_utf8(
|
|
658
|
+
text,
|
|
659
|
+
TRUSTED_PRESENTATION_MAX_KEY_BYTES - len(digest) - 2,
|
|
660
|
+
)
|
|
661
|
+
return f"{prefix}~{digest}"
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _bounded_path(prefix: str, segments: Sequence[str]) -> str:
|
|
665
|
+
joined = "/".join((*((prefix.lstrip("/"),) if prefix else ()), *segments))
|
|
666
|
+
path = f"/{joined}" if joined else "/"
|
|
667
|
+
encoded = path.encode("utf-8", errors="replace")
|
|
668
|
+
if len(encoded) <= TRUSTED_PRESENTATION_MAX_PATH_BYTES:
|
|
669
|
+
return path
|
|
670
|
+
digest = hashlib.sha256(encoded).hexdigest()
|
|
671
|
+
retained, _ = _truncate_utf8(
|
|
672
|
+
path,
|
|
673
|
+
TRUSTED_PRESENTATION_MAX_PATH_BYTES - len(digest) - 2,
|
|
674
|
+
)
|
|
675
|
+
return f"{retained}~{digest}"
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _utf8_tail(text: str, limit: int) -> str:
|
|
679
|
+
if limit <= 0:
|
|
680
|
+
return ""
|
|
681
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
682
|
+
if len(encoded) <= limit:
|
|
683
|
+
return text
|
|
684
|
+
tail = encoded[-limit:]
|
|
685
|
+
while tail:
|
|
686
|
+
try:
|
|
687
|
+
return tail.decode("utf-8")
|
|
688
|
+
except UnicodeDecodeError as error:
|
|
689
|
+
tail = tail[error.end :]
|
|
690
|
+
return ""
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _shortened_value_text(
|
|
694
|
+
text: str,
|
|
695
|
+
limit: int,
|
|
696
|
+
*,
|
|
697
|
+
head_and_tail: bool,
|
|
698
|
+
available: bool,
|
|
699
|
+
) -> str:
|
|
700
|
+
suffix = _VIEW_FULL_SUFFIX if available else " ... [complete value unavailable]"
|
|
701
|
+
suffix_bytes = len(suffix.encode("utf-8"))
|
|
702
|
+
if suffix_bytes >= limit:
|
|
703
|
+
return _truncate_utf8(suffix, limit)[0]
|
|
704
|
+
body_limit = limit - suffix_bytes
|
|
705
|
+
if not head_and_tail:
|
|
706
|
+
prefix, _ = _truncate_utf8(text, body_limit)
|
|
707
|
+
return f"{prefix}{suffix}"
|
|
708
|
+
|
|
709
|
+
separator = " ... "
|
|
710
|
+
separator_bytes = len(separator.encode("utf-8"))
|
|
711
|
+
if separator_bytes >= body_limit:
|
|
712
|
+
prefix, _ = _truncate_utf8(text, body_limit)
|
|
713
|
+
return f"{prefix}{suffix}"
|
|
714
|
+
remaining = body_limit - separator_bytes
|
|
715
|
+
head_limit = max(1, remaining // 3)
|
|
716
|
+
tail_limit = remaining - head_limit
|
|
717
|
+
head, _ = _truncate_utf8(text, head_limit)
|
|
718
|
+
if head.endswith("..."):
|
|
719
|
+
head = head[:-3]
|
|
720
|
+
tail = _utf8_tail(text, tail_limit)
|
|
721
|
+
return f"{head}{separator}{tail}{suffix}"
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _shortened_key_text(text: str, limit: int, *, available: bool) -> str:
|
|
725
|
+
suffix = _VIEW_FULL_KEY_SUFFIX if available else " ... [complete key unavailable]"
|
|
726
|
+
suffix_bytes = len(suffix.encode("utf-8"))
|
|
727
|
+
if suffix_bytes >= limit:
|
|
728
|
+
return _truncate_utf8(suffix, limit)[0]
|
|
729
|
+
prefix, _ = _truncate_utf8(text, limit - suffix_bytes)
|
|
730
|
+
if prefix.endswith("..."):
|
|
731
|
+
prefix = prefix[:-3]
|
|
732
|
+
return f"{prefix}{suffix}"
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _truncate_utf8(text: str, limit: int) -> tuple[str, bool]:
|
|
736
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
737
|
+
if len(encoded) <= limit:
|
|
738
|
+
return text, False
|
|
739
|
+
if limit <= 0:
|
|
740
|
+
return "", True
|
|
741
|
+
marker = b"..." if limit >= 3 else b""
|
|
742
|
+
prefix = encoded[: max(0, limit - len(marker))]
|
|
743
|
+
while prefix:
|
|
744
|
+
try:
|
|
745
|
+
decoded = prefix.decode("utf-8")
|
|
746
|
+
break
|
|
747
|
+
except UnicodeDecodeError as error:
|
|
748
|
+
prefix = prefix[: error.start]
|
|
749
|
+
else:
|
|
750
|
+
decoded = ""
|
|
751
|
+
return decoded + marker.decode("ascii"), True
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _bounded_field_value(value: object) -> tuple[PresentationValue, bool]:
|
|
755
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
756
|
+
return value, False
|
|
757
|
+
if isinstance(value, str):
|
|
758
|
+
return _truncate_utf8(value, TRUSTED_PRESENTATION_MAX_FIELD_VALUE_BYTES)
|
|
759
|
+
if isinstance(value, bytes):
|
|
760
|
+
return f"[binary value omitted: {len(value)} bytes]", True
|
|
761
|
+
if isinstance(value, (list, tuple)):
|
|
762
|
+
output: list[PresentationScalar] = []
|
|
763
|
+
truncated = len(value) > TRUSTED_PRESENTATION_MAX_SEQUENCE_ITEMS
|
|
764
|
+
for item in value[:TRUSTED_PRESENTATION_MAX_SEQUENCE_ITEMS]:
|
|
765
|
+
if item is None or isinstance(item, (bool, int, float)):
|
|
766
|
+
output.append(item)
|
|
767
|
+
elif isinstance(item, str):
|
|
768
|
+
bounded, was_truncated = _truncate_utf8(
|
|
769
|
+
item, TRUSTED_PRESENTATION_MAX_FIELD_VALUE_BYTES
|
|
770
|
+
)
|
|
771
|
+
output.append(bounded)
|
|
772
|
+
truncated = truncated or was_truncated
|
|
773
|
+
elif isinstance(item, bytes):
|
|
774
|
+
output.append(f"[binary value omitted: {len(item)} bytes]")
|
|
775
|
+
truncated = True
|
|
776
|
+
else:
|
|
777
|
+
output.append("[nested value omitted; see Raw tab]")
|
|
778
|
+
truncated = True
|
|
779
|
+
return tuple(output), truncated
|
|
780
|
+
return "[nested value omitted; see Raw tab]", True
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def bounded_presentation_text(
|
|
784
|
+
value: object,
|
|
785
|
+
*,
|
|
786
|
+
limit: int = TRUSTED_PRESENTATION_MAX_PARAMETER_TEXT_BYTES,
|
|
787
|
+
) -> tuple[str, bool]:
|
|
788
|
+
"""Format one table identifier/label without retaining unbounded text."""
|
|
789
|
+
|
|
790
|
+
if isinstance(value, bytes):
|
|
791
|
+
return f"[binary value omitted: {len(value)} bytes]", True
|
|
792
|
+
if not isinstance(value, str):
|
|
793
|
+
value = "" if value is None else str(value)
|
|
794
|
+
return _truncate_utf8(value, limit)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def bounded_presentation_scalar(
|
|
798
|
+
value: object,
|
|
799
|
+
) -> tuple[PresentationScalar, bool]:
|
|
800
|
+
"""Bound one scalar used by a Qt cell and its tooltip."""
|
|
801
|
+
|
|
802
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
803
|
+
return value, False
|
|
804
|
+
if isinstance(value, bytes):
|
|
805
|
+
return f"[binary value omitted: {len(value)} bytes]", True
|
|
806
|
+
if isinstance(value, str):
|
|
807
|
+
return _truncate_utf8(value, TRUSTED_PRESENTATION_MAX_FIELD_VALUE_BYTES)
|
|
808
|
+
return f"[{type(value).__name__} value unavailable]", True
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def bounded_presentation_names(
|
|
812
|
+
values: Sequence[object],
|
|
813
|
+
) -> tuple[tuple[str, ...], bool]:
|
|
814
|
+
"""Bound an identifier list retained by the GUI-delivered detail object."""
|
|
815
|
+
|
|
816
|
+
output: list[str] = []
|
|
817
|
+
truncated = len(values) > TRUSTED_PRESENTATION_MAX_UNAVAILABLE_FIELDS
|
|
818
|
+
for value in values[:TRUSTED_PRESENTATION_MAX_UNAVAILABLE_FIELDS]:
|
|
819
|
+
bounded, was_truncated = bounded_presentation_text(value)
|
|
820
|
+
output.append(bounded)
|
|
821
|
+
truncated = truncated or was_truncated
|
|
822
|
+
if truncated:
|
|
823
|
+
if len(output) >= TRUSTED_PRESENTATION_MAX_UNAVAILABLE_FIELDS:
|
|
824
|
+
output.pop()
|
|
825
|
+
output.append("[additional unavailable fields omitted]")
|
|
826
|
+
return tuple(dict.fromkeys(output)), truncated
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def bounded_presentation_error(value: object) -> str:
|
|
830
|
+
"""Return one bounded failure string safe for queued Qt publication."""
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
message = str(value).strip()
|
|
834
|
+
except Exception:
|
|
835
|
+
message = f"{type(value).__name__} details are unavailable."
|
|
836
|
+
if not message:
|
|
837
|
+
message = "Run details are unavailable."
|
|
838
|
+
bounded, _truncated = _truncate_utf8(
|
|
839
|
+
message,
|
|
840
|
+
TRUSTED_PRESENTATION_MAX_ERROR_BYTES,
|
|
841
|
+
)
|
|
842
|
+
return bounded
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def bounded_selected_run_fields(
|
|
846
|
+
fields: Mapping[str, object],
|
|
847
|
+
) -> FrozenPresentationFields:
|
|
848
|
+
"""Retain only bounded run fields needed by overview, actions, and rows."""
|
|
849
|
+
|
|
850
|
+
output: list[tuple[str, PresentationValue]] = []
|
|
851
|
+
truncated = False
|
|
852
|
+
retained_bytes = 0
|
|
853
|
+
for name in _SELECTED_RUN_FIELDS[:TRUSTED_PRESENTATION_MAX_RUN_FIELDS]:
|
|
854
|
+
if name not in fields:
|
|
855
|
+
continue
|
|
856
|
+
bounded, was_truncated = _bounded_field_value(fields[name])
|
|
857
|
+
field_bytes = len(name.encode("utf-8")) + _presentation_value_bytes(bounded)
|
|
858
|
+
if (
|
|
859
|
+
retained_bytes + field_bytes
|
|
860
|
+
> TRUSTED_PRESENTATION_MAX_COMPATIBILITY_TEXT_BYTES
|
|
861
|
+
):
|
|
862
|
+
truncated = True
|
|
863
|
+
break
|
|
864
|
+
output.append((name, bounded))
|
|
865
|
+
retained_bytes += field_bytes
|
|
866
|
+
truncated = truncated or was_truncated
|
|
867
|
+
if truncated:
|
|
868
|
+
_make_room_for_field_marker(
|
|
869
|
+
output,
|
|
870
|
+
retained_bytes,
|
|
871
|
+
_RUN_TRUNCATION_FIELD,
|
|
872
|
+
max_items=TRUSTED_PRESENTATION_MAX_RUN_FIELDS,
|
|
873
|
+
)
|
|
874
|
+
output.append(_RUN_TRUNCATION_FIELD)
|
|
875
|
+
return tuple(output)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def bounded_metadata_fields(
|
|
879
|
+
fields: Mapping[str, object],
|
|
880
|
+
) -> FrozenPresentationFields:
|
|
881
|
+
"""Return a small compatibility mapping without retaining raw metadata."""
|
|
882
|
+
|
|
883
|
+
output: list[tuple[str, PresentationValue]] = []
|
|
884
|
+
truncated = len(fields) > TRUSTED_PRESENTATION_MAX_METADATA_FIELDS
|
|
885
|
+
seen_keys: set[str] = set()
|
|
886
|
+
retained_bytes = 0
|
|
887
|
+
for index, (name, value) in enumerate(fields.items()):
|
|
888
|
+
if index >= TRUSTED_PRESENTATION_MAX_METADATA_FIELDS:
|
|
889
|
+
break
|
|
890
|
+
bounded_name, key_truncated = _truncate_utf8(
|
|
891
|
+
_key_text(name), TRUSTED_PRESENTATION_MAX_KEY_BYTES
|
|
892
|
+
)
|
|
893
|
+
if bounded_name in seen_keys:
|
|
894
|
+
truncated = True
|
|
895
|
+
continue
|
|
896
|
+
seen_keys.add(bounded_name)
|
|
897
|
+
bounded_value, value_truncated = _bounded_field_value(value)
|
|
898
|
+
field_bytes = len(bounded_name.encode("utf-8")) + _presentation_value_bytes(
|
|
899
|
+
bounded_value
|
|
900
|
+
)
|
|
901
|
+
if (
|
|
902
|
+
retained_bytes + field_bytes
|
|
903
|
+
> TRUSTED_PRESENTATION_MAX_COMPATIBILITY_TEXT_BYTES
|
|
904
|
+
):
|
|
905
|
+
truncated = True
|
|
906
|
+
break
|
|
907
|
+
output.append((bounded_name, bounded_value))
|
|
908
|
+
retained_bytes += field_bytes
|
|
909
|
+
truncated = truncated or key_truncated or value_truncated
|
|
910
|
+
if truncated:
|
|
911
|
+
marker_name = _TRUNCATION_KEY
|
|
912
|
+
while marker_name in seen_keys:
|
|
913
|
+
marker_name += "*"
|
|
914
|
+
marker = (marker_name, _METADATA_TRUNCATION_VALUE)
|
|
915
|
+
_make_room_for_field_marker(
|
|
916
|
+
output,
|
|
917
|
+
retained_bytes,
|
|
918
|
+
marker,
|
|
919
|
+
max_items=TRUSTED_PRESENTATION_MAX_METADATA_FIELDS,
|
|
920
|
+
)
|
|
921
|
+
output.append(marker)
|
|
922
|
+
return tuple(output)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _make_room_for_field_marker(
|
|
926
|
+
output: list[tuple[str, PresentationValue]],
|
|
927
|
+
retained_bytes: int,
|
|
928
|
+
marker: tuple[str, PresentationValue],
|
|
929
|
+
*,
|
|
930
|
+
max_items: int,
|
|
931
|
+
) -> None:
|
|
932
|
+
marker_bytes = _presentation_field_bytes(*marker)
|
|
933
|
+
while output and (
|
|
934
|
+
len(output) >= max_items
|
|
935
|
+
or retained_bytes + marker_bytes
|
|
936
|
+
> TRUSTED_PRESENTATION_MAX_COMPATIBILITY_TEXT_BYTES
|
|
937
|
+
):
|
|
938
|
+
removed_name, removed_value = output.pop()
|
|
939
|
+
retained_bytes -= _presentation_field_bytes(removed_name, removed_value)
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
def _presentation_field_bytes(name: str, value: PresentationValue) -> int:
|
|
943
|
+
return len(name.encode("utf-8")) + _presentation_value_bytes(value)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _presentation_value_bytes(value: PresentationValue) -> int:
|
|
947
|
+
if isinstance(value, tuple):
|
|
948
|
+
return sum(len(_scalar_text(item).encode("utf-8")) for item in value)
|
|
949
|
+
return len(_scalar_text(value).encode("utf-8"))
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def normalize_presentation_tree(value: object) -> TrustedPresentationView:
|
|
953
|
+
"""Flatten a nested value iteratively under all presentation budgets."""
|
|
954
|
+
|
|
955
|
+
return _PresentationNormalizer(value).normalize()
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def build_selected_run_presentation(
|
|
959
|
+
*,
|
|
960
|
+
run_fields: Mapping[str, object],
|
|
961
|
+
metadata_fields: Mapping[str, object],
|
|
962
|
+
parameters: Sequence[Mapping[str, object]],
|
|
963
|
+
snapshot_summary: Mapping[str, object],
|
|
964
|
+
setpoint_summaries: Sequence[Mapping[str, object]],
|
|
965
|
+
unavailable_fields: Sequence[str],
|
|
966
|
+
parameters_truncated: bool = False,
|
|
967
|
+
) -> TrustedSelectedRunPresentation:
|
|
968
|
+
"""Build every non-snapshot tree/table input before crossing into Qt."""
|
|
969
|
+
|
|
970
|
+
raw_run = {name: value for name, value in run_fields.items() if name != "snapshot"}
|
|
971
|
+
raw_value = {
|
|
972
|
+
"Run": raw_run,
|
|
973
|
+
"Metadata": metadata_fields,
|
|
974
|
+
"Snapshot": snapshot_summary,
|
|
975
|
+
"Parameters": parameters,
|
|
976
|
+
"Setpoint summaries": setpoint_summaries,
|
|
977
|
+
"Unavailable fields": unavailable_fields,
|
|
978
|
+
}
|
|
979
|
+
if parameters_truncated:
|
|
980
|
+
raw_value["Parameters status"] = (
|
|
981
|
+
"Additional or oversized parameter details were omitted at "
|
|
982
|
+
"presentation limits."
|
|
983
|
+
)
|
|
984
|
+
full_values = _FullValueRegistry()
|
|
985
|
+
metadata = _PresentationNormalizer(
|
|
986
|
+
metadata_fields,
|
|
987
|
+
full_values=full_values,
|
|
988
|
+
path_prefix="Metadata",
|
|
989
|
+
).normalize()
|
|
990
|
+
initial_reasons: list[str] = []
|
|
991
|
+
presentation_omissions = parameters_truncated or any(
|
|
992
|
+
name.endswith(".presentation") for name in unavailable_fields
|
|
993
|
+
)
|
|
994
|
+
upstream_unavailable = any(
|
|
995
|
+
not name.endswith(".presentation") for name in unavailable_fields
|
|
996
|
+
)
|
|
997
|
+
if presentation_omissions:
|
|
998
|
+
initial_reasons.append(
|
|
999
|
+
"One or more selected-run structures were omitted at presentation limits."
|
|
1000
|
+
)
|
|
1001
|
+
if upstream_unavailable:
|
|
1002
|
+
initial_reasons.append(
|
|
1003
|
+
"One or more selected-run fields were unavailable upstream; complete "
|
|
1004
|
+
"source data was omitted."
|
|
1005
|
+
)
|
|
1006
|
+
snapshot_status = snapshot_summary.get("Status")
|
|
1007
|
+
if isinstance(snapshot_status, str) and snapshot_status.casefold() in {
|
|
1008
|
+
"truncated",
|
|
1009
|
+
"malformed",
|
|
1010
|
+
"unavailable",
|
|
1011
|
+
}:
|
|
1012
|
+
initial_reasons.append(
|
|
1013
|
+
"Snapshot source data was truncated, malformed, or unavailable."
|
|
1014
|
+
)
|
|
1015
|
+
raw = _PresentationNormalizer(
|
|
1016
|
+
raw_value,
|
|
1017
|
+
full_values=full_values,
|
|
1018
|
+
path_prefix="Raw",
|
|
1019
|
+
initial_reasons=initial_reasons,
|
|
1020
|
+
).normalize()
|
|
1021
|
+
return TrustedSelectedRunPresentation(
|
|
1022
|
+
run_fields=bounded_selected_run_fields(run_fields),
|
|
1023
|
+
metadata_fields=bounded_metadata_fields(metadata_fields),
|
|
1024
|
+
metadata=metadata,
|
|
1025
|
+
raw=raw,
|
|
1026
|
+
parameters_truncated=parameters_truncated,
|
|
1027
|
+
full_values=full_values.freeze(),
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
__all__ = [
|
|
1032
|
+
"TRUSTED_PRESENTATION_MAX_CONTAINER_ITEMS",
|
|
1033
|
+
"TRUSTED_PRESENTATION_MAX_COMPATIBILITY_TEXT_BYTES",
|
|
1034
|
+
"TRUSTED_PRESENTATION_MAX_DEPTH",
|
|
1035
|
+
"TRUSTED_PRESENTATION_MAX_ERROR_BYTES",
|
|
1036
|
+
"TRUSTED_PRESENTATION_MAX_FIELD_VALUE_BYTES",
|
|
1037
|
+
"TRUSTED_PRESENTATION_MAX_FULL_VALUE_TEXT_BYTES",
|
|
1038
|
+
"TRUSTED_PRESENTATION_MAX_KEY_BYTES",
|
|
1039
|
+
"TRUSTED_PRESENTATION_MAX_METADATA_FIELDS",
|
|
1040
|
+
"TRUSTED_PRESENTATION_MAX_PARAMETERS",
|
|
1041
|
+
"TRUSTED_PRESENTATION_MAX_PATH_BYTES",
|
|
1042
|
+
"TRUSTED_PRESENTATION_MAX_PARAMETER_TOTAL_TEXT_BYTES",
|
|
1043
|
+
"TRUSTED_PRESENTATION_MAX_PARAMETER_DEPENDENCIES",
|
|
1044
|
+
"TRUSTED_PRESENTATION_MAX_PARAMETER_TEXT_BYTES",
|
|
1045
|
+
"TRUSTED_PRESENTATION_MAX_RENDERED_NODES",
|
|
1046
|
+
"TRUSTED_PRESENTATION_MAX_RENDERED_TEXT_BYTES",
|
|
1047
|
+
"TRUSTED_PRESENTATION_MAX_RUN_FIELDS",
|
|
1048
|
+
"TRUSTED_PRESENTATION_MAX_SEQUENCE_ITEMS",
|
|
1049
|
+
"TRUSTED_PRESENTATION_MAX_TOOLTIP_BYTES",
|
|
1050
|
+
"TRUSTED_PRESENTATION_MAX_TOOLTIP_TEXT_BYTES",
|
|
1051
|
+
"TRUSTED_PRESENTATION_MAX_UNAVAILABLE_FIELDS",
|
|
1052
|
+
"TRUSTED_PRESENTATION_MAX_VALUE_BYTES",
|
|
1053
|
+
"TrustedPresentationNode",
|
|
1054
|
+
"TrustedPresentationFullValue",
|
|
1055
|
+
"TrustedPresentationView",
|
|
1056
|
+
"TrustedSelectedRunPresentation",
|
|
1057
|
+
"bounded_metadata_fields",
|
|
1058
|
+
"bounded_presentation_scalar",
|
|
1059
|
+
"bounded_presentation_names",
|
|
1060
|
+
"bounded_presentation_error",
|
|
1061
|
+
"bounded_presentation_text",
|
|
1062
|
+
"bounded_selected_run_fields",
|
|
1063
|
+
"build_selected_run_presentation",
|
|
1064
|
+
"normalize_presentation_tree",
|
|
1065
|
+
]
|