quantized-lab 0.8.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.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Results-log + notes-window recovery from Origin project files (plan item 6).
|
|
2
|
+
|
|
3
|
+
Origin's *results log* — the running record of every analysis operation
|
|
4
|
+
(fits, subtractions, smoothing) with its parameters and outputs — is stored
|
|
5
|
+
as plain text in the windows section of both containers, as timestamped
|
|
6
|
+
records shaped like::
|
|
7
|
+
|
|
8
|
+
[5/6/2019 15:16:34 "" (2458609)]
|
|
9
|
+
subtract_line(subtract_line)
|
|
10
|
+
Input
|
|
11
|
+
iy(Input) = [Book4]Sheet1!(C"H",M)
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
Because the records are plain text in both ``.opj`` (CPYA) and ``.opju``
|
|
15
|
+
(CPYUA), one byte-level scan serves both: collect printable runs that
|
|
16
|
+
contain at least one timestamp record header. This is fit *provenance*
|
|
17
|
+
worth surfacing, not data — it lands in ``metadata['origin_results_log']``.
|
|
18
|
+
|
|
19
|
+
That raw text is further parsed into structured per-operation records by
|
|
20
|
+
``parse_results_log`` (plan item 22): each record is
|
|
21
|
+
``{"timestamp": "M/D/YYYY H:MM:SS", "operation": str, "params": {...}}``
|
|
22
|
+
where ``params`` nests by the log's ``Input``/``Output``/etc. section
|
|
23
|
+
headers, e.g. ``params["Input"]["iy"] == '[Book4]Sheet1!(C"H",M)'``.
|
|
24
|
+
Parameter lines that precede any section header land under the empty-
|
|
25
|
+
string section ``params[""]``. Lines inside a record that don't parse as
|
|
26
|
+
a timestamp header, an operation line, a section header, or a
|
|
27
|
+
``key(display) = value`` parameter are never dropped silently — they
|
|
28
|
+
collect in that record's ``"extra"`` list (omitted when empty). A record
|
|
29
|
+
with no operation line still yields its timestamp, with
|
|
30
|
+
``operation == ""``. This lands in
|
|
31
|
+
``metadata['origin_results_log_records']`` alongside the raw text.
|
|
32
|
+
|
|
33
|
+
*Notes windows* (free-form user text pages) sit in the ``.opju`` (CPYUA)
|
|
34
|
+
windows section as a tight, contiguous pair of length-prefixed records::
|
|
35
|
+
|
|
36
|
+
93 <nl> <window-name> 00 0a <tl-varint> <note-text> 00
|
|
37
|
+
|
|
38
|
+
— a ``0x93`` window-name record (``nl`` counts name+NUL) whose NUL butts
|
|
39
|
+
directly against a ``0x0a`` text record. ``tl`` is a **LEB128 varint**
|
|
40
|
+
counting text+NUL (a 718-byte note stores ``ce 05``): the original
|
|
41
|
+
single-byte read silently dropped every note past ~127 chars, fixed
|
|
42
|
+
2026-07-06 against ``notes_real.opju`` (a real 717-char note loaded via
|
|
43
|
+
``open -n``, holding Windows paths and inequalities). Text decodes
|
|
44
|
+
UTF-8-first (latin-1 fallback), and the internal-junk filter keys on named
|
|
45
|
+
OriginStorage/CDATA tokens only — NOT bare ``\``/``<``/``>``, which real
|
|
46
|
+
notes legitimately carry. It still matches **zero** records across the whole
|
|
47
|
+
real corpus (none carry a notes window), so it attaches nothing
|
|
48
|
+
speculatively — the false-positive bar the earlier log-only scan set. Notes
|
|
49
|
+
land in ``metadata['origin_notes']`` as ``{window_name: text}``. The scan is
|
|
50
|
+
byte-level so it also runs over ``.opj`` (CPYA), where it is likewise
|
|
51
|
+
false-positive-clean on the corpus but has no known-content oracle; Origin
|
|
52
|
+
2023+ cannot write ``.opj`` so no such specimen can be produced.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import re
|
|
58
|
+
|
|
59
|
+
from quantized.io.origin_project.tree import _find_tail_start, _TailParseError
|
|
60
|
+
|
|
61
|
+
__all__ = ["notes_windows", "parse_results_log", "results_log"]
|
|
62
|
+
|
|
63
|
+
# One timestamped operation-record header, e.g. `[5/6/2019 15:16:34 "" (2458609)]`.
|
|
64
|
+
_RECORD = re.compile(rb"\[\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2}:\d{2}[^\]\r\n]{0,80}\]")
|
|
65
|
+
|
|
66
|
+
# A printable text run long enough to hold at least one record.
|
|
67
|
+
_RUN = re.compile(rb"[\x20-\x7e\r\n\t]{40,}")
|
|
68
|
+
|
|
69
|
+
_MAX_LOG = 200_000 # metadata guard: never attach unbounded text
|
|
70
|
+
|
|
71
|
+
# Text containing any of these is an internal storage blob, not a user note.
|
|
72
|
+
# Bare `\`, `<`, `>` were REMOVED 2026-07-06: real notes legitimately hold
|
|
73
|
+
# Windows paths ("C:\lab\data") and inequalities ("T < 4 K", "H > 2 T"); the
|
|
74
|
+
# named storage tokens below still reject every OriginStorage/CDATA XML blob
|
|
75
|
+
# (corpus stays false-positive-clean). `</` catches XML close-tags without
|
|
76
|
+
# hitting a lone inequality.
|
|
77
|
+
_NOTE_JUNK = (b"OriginStorage", b"ColumnInfo", b"ImportFile", b"CDATA", b"</", b"<NEWBOOK")
|
|
78
|
+
_MAX_NOTES = 64 # sane cap on notes windows per project
|
|
79
|
+
_MAX_NOTE_LEN = 100_000 # a note is free-form prose; bounded, not a 250-byte cap
|
|
80
|
+
|
|
81
|
+
_WINDOW_MARK = re.compile(rb"\x93")
|
|
82
|
+
|
|
83
|
+
# Safety cushion (bytes) subtracted from the structurally-derived tail
|
|
84
|
+
# boundary (`tree._find_tail_start`) before scanning a `.opj` for a results
|
|
85
|
+
# log / notes window (see `_tail_scan_start`). Never a hardcoded byte offset
|
|
86
|
+
# or file-size percentage -- always relative to THIS file's own computed
|
|
87
|
+
# block-stream-end position. Sized against the real corpus
|
|
88
|
+
# (`../test-data/origin`): every project there with a results log has its
|
|
89
|
+
# first match within ~550 bytes of that boundary (XMCD.opj, the largest hit,
|
|
90
|
+
# is 521 bytes past it), so 2 MiB leaves a wide cushion while still skipping
|
|
91
|
+
# the ~90%+ of a large project (PNR.opj: 127 MB file, 116.5 MB boundary)
|
|
92
|
+
# that is bulk float data and structurally cannot contain a match.
|
|
93
|
+
_TAIL_SAFETY_MARGIN = 2 * 1024 * 1024
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _tail_scan_start(b: bytes, suffix: str) -> int:
|
|
97
|
+
"""Where `results_log`/`notes_windows` should start scanning ``b``.
|
|
98
|
+
|
|
99
|
+
Restricted to ``.opj`` (CPYA): its block-stream framing lets
|
|
100
|
+
`tree._find_tail_start` locate exactly where the free-text tail begins
|
|
101
|
+
(results-log records and notes windows both live there -- see the
|
|
102
|
+
module docstring's ``_skip_notes`` reference), and the real corpus
|
|
103
|
+
confirms every match sits within a few hundred bytes of that boundary.
|
|
104
|
+
|
|
105
|
+
``.opju`` (CPYUA) uses a different, FPC-compressed column codec with no
|
|
106
|
+
equivalent boundary function, and the corpus has no full-size `.opju`
|
|
107
|
+
results-log specimen to verify a tail-only property against (its two
|
|
108
|
+
notes-window specimens are ~4 KB, too small for a restriction to matter)
|
|
109
|
+
-- so it (and any other/unknown suffix) keeps the full-buffer scan.
|
|
110
|
+
``suffix=""`` (the default on both public functions) always full-scans,
|
|
111
|
+
so direct callers that don't know/care about the container type --
|
|
112
|
+
including every synthetic-bytes test in ``test_io_origin_project.py`` --
|
|
113
|
+
are unaffected.
|
|
114
|
+
|
|
115
|
+
Never raises: a tail that fails to parse (older/corrupt/unexpected
|
|
116
|
+
containers) degrades to a full scan, same "degrade, never guess"
|
|
117
|
+
convention as `tree.opj_project_dates`/`tree.opj_folder_paths`.
|
|
118
|
+
"""
|
|
119
|
+
if suffix != ".opj":
|
|
120
|
+
return 0
|
|
121
|
+
try:
|
|
122
|
+
tail = _find_tail_start(b)
|
|
123
|
+
except _TailParseError:
|
|
124
|
+
return 0
|
|
125
|
+
return max(0, tail - _TAIL_SAFETY_MARGIN)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def results_log(b: bytes, *, suffix: str = "") -> str:
|
|
129
|
+
"""The project's results-log text, or ``""`` when none is present.
|
|
130
|
+
|
|
131
|
+
Only printable runs containing a timestamped record header qualify —
|
|
132
|
+
OriginStorage XML, LabTalk scripts, and other internal text never match
|
|
133
|
+
the record shape, so nothing is scraped speculatively. ``suffix`` (e.g.
|
|
134
|
+
``".opj"``) restricts the scan to the structurally-derived tail where a
|
|
135
|
+
match can actually occur (see `_tail_scan_start`); omit it (or pass
|
|
136
|
+
``""``) to scan the whole buffer.
|
|
137
|
+
"""
|
|
138
|
+
start = _tail_scan_start(b, suffix)
|
|
139
|
+
parts: list[str] = []
|
|
140
|
+
total = 0
|
|
141
|
+
for m in _RUN.finditer(b, start):
|
|
142
|
+
run = m.group(0)
|
|
143
|
+
if not _RECORD.search(run):
|
|
144
|
+
continue
|
|
145
|
+
text = run.decode("latin1").strip()
|
|
146
|
+
parts.append(text)
|
|
147
|
+
total += len(text)
|
|
148
|
+
if total >= _MAX_LOG:
|
|
149
|
+
break
|
|
150
|
+
return "\n\n".join(parts)[:_MAX_LOG]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _printable(data: bytes, *, allow_newlines: bool = False) -> bool:
|
|
154
|
+
lo = 0x09 if allow_newlines else 0x20
|
|
155
|
+
return bool(data) and all(lo <= c <= 0x0D or 0x20 <= c <= 0x7E for c in data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _read_varint(b: bytes, p: int) -> tuple[int | None, int]:
|
|
159
|
+
"""LEB128 length varint at ``p`` -> ``(value, next_pos)``.
|
|
160
|
+
|
|
161
|
+
Returns ``(None, p)`` if it isn't a well-formed varint within 3 bytes. The
|
|
162
|
+
``.opju`` text record's length prefix GROWS with the note (a 718-byte note
|
|
163
|
+
stores ``ce 05``), so a single-byte read silently dropped every note past
|
|
164
|
+
~127 chars -- the same varint-width class as the big-column fix (audit
|
|
165
|
+
#16), here confirmed by ``notes_real.opju`` (2026-07-06)."""
|
|
166
|
+
val = shift = 0
|
|
167
|
+
for k in range(3):
|
|
168
|
+
if p + k >= len(b):
|
|
169
|
+
return None, p
|
|
170
|
+
c = b[p + k]
|
|
171
|
+
val |= (c & 0x7F) << shift
|
|
172
|
+
if not c & 0x80:
|
|
173
|
+
return val, p + k + 1
|
|
174
|
+
shift += 7
|
|
175
|
+
return None, p
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _decode_note(raw: bytes) -> str | None:
|
|
179
|
+
"""UTF-8-first (latin-1 fallback) note text, ``\\r\\n`` normalized, or
|
|
180
|
+
``None`` if it isn't printable text. The ``.opju`` container is UTF-8, so a
|
|
181
|
+
note with a degree sign / Greek survives."""
|
|
182
|
+
for enc in ("utf-8", "latin1"):
|
|
183
|
+
try:
|
|
184
|
+
s = raw.decode(enc)
|
|
185
|
+
except UnicodeDecodeError:
|
|
186
|
+
continue
|
|
187
|
+
if any(ord(c) < 0x20 and c not in "\t\r\n" for c in s):
|
|
188
|
+
return None
|
|
189
|
+
return s.replace("\r\n", "\n")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def notes_windows(b: bytes, *, suffix: str = "") -> dict[str, str]:
|
|
194
|
+
"""Map notes-window name -> its free text (``\\r\\n`` normalized to ``\\n``).
|
|
195
|
+
|
|
196
|
+
Recognizes only the exact contiguous ``93 <nl> <name> 00 0a <tl> <text>
|
|
197
|
+
00`` framing (see module docstring). Every candidate must pass a
|
|
198
|
+
printable-character + internal-junk-token filter, so OriginStorage XML
|
|
199
|
+
and storage blobs never masquerade as user notes. Returns ``{}`` when no
|
|
200
|
+
notes window is present. ``suffix`` restricts the scan the same way
|
|
201
|
+
`results_log` does -- see `_tail_scan_start`.
|
|
202
|
+
"""
|
|
203
|
+
out: dict[str, str] = {}
|
|
204
|
+
n = len(b)
|
|
205
|
+
start = _tail_scan_start(b, suffix)
|
|
206
|
+
for m in _WINDOW_MARK.finditer(b, start):
|
|
207
|
+
p = m.start()
|
|
208
|
+
if p + 2 >= n:
|
|
209
|
+
continue
|
|
210
|
+
nl = b[p + 1]
|
|
211
|
+
if not (2 <= nl <= 64) or p + 1 + nl >= n or b[p + 1 + nl] != 0:
|
|
212
|
+
continue
|
|
213
|
+
name = b[p + 2 : p + 1 + nl] # nl-1 bytes + the NUL just checked
|
|
214
|
+
if not _printable(name):
|
|
215
|
+
continue
|
|
216
|
+
q = p + 2 + nl # first byte after the name's NUL
|
|
217
|
+
if q + 1 >= n or b[q] != 0x0A:
|
|
218
|
+
continue
|
|
219
|
+
tl, text_start = _read_varint(b, q + 1) # tl counts text + trailing NUL
|
|
220
|
+
if tl is None or not (2 <= tl <= _MAX_NOTE_LEN):
|
|
221
|
+
continue
|
|
222
|
+
text_end = text_start + tl - 1 # index of the trailing NUL
|
|
223
|
+
if text_end >= n or b[text_end] != 0:
|
|
224
|
+
continue
|
|
225
|
+
text = b[text_start:text_end]
|
|
226
|
+
if any(j in text for j in _NOTE_JUNK):
|
|
227
|
+
continue
|
|
228
|
+
decoded = _decode_note(text)
|
|
229
|
+
if decoded is None:
|
|
230
|
+
continue
|
|
231
|
+
out[name.decode("latin1")] = decoded
|
|
232
|
+
if len(out) >= _MAX_NOTES:
|
|
233
|
+
break
|
|
234
|
+
return out
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ── structured results-log parsing (plan item 22) ────────────────────────────
|
|
238
|
+
|
|
239
|
+
# A results-log record header, on ``results_log()``'s decoded str text (see
|
|
240
|
+
# ``_RECORD`` above for the byte-level equivalent used to find the raw runs).
|
|
241
|
+
_LOG_HEADER = re.compile(r"\[(\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2}:\d{2})[^\[\]\r\n]{0,80}\]")
|
|
242
|
+
|
|
243
|
+
# A bare operation line, e.g. `subtract_line(subtract_line)`: no `=`, so it
|
|
244
|
+
# never collides with a `key(display) = value` parameter line.
|
|
245
|
+
_OPERATION_LINE = re.compile(r"^([A-Za-z_]\w*)\([^)]*\)\s*$")
|
|
246
|
+
|
|
247
|
+
# A `key(display) = value` (or plain `key = value`) parameter line.
|
|
248
|
+
_PARAM_LINE = re.compile(r"^([A-Za-z_]\w*)(?:\([^)]*\))?\s*=\s*(.*)$")
|
|
249
|
+
|
|
250
|
+
# A section header, e.g. `Input` / `Output`: a short word/phrase, no `=`.
|
|
251
|
+
_SECTION_LINE = re.compile(r"^[A-Za-z][A-Za-z0-9_ ]{0,40}$")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def parse_results_log(text: str) -> list[dict[str, object]]:
|
|
255
|
+
"""Parse ``results_log()``'s raw text into structured per-operation records.
|
|
256
|
+
|
|
257
|
+
Each record is ``{"timestamp": str, "operation": str, "params": dict}``,
|
|
258
|
+
optionally with an ``"extra"`` list of lines inside the record that didn't
|
|
259
|
+
match any recognized shape (never dropped silently). ``params`` nests by
|
|
260
|
+
the log's section headers (``Input``/``Output``/etc.); parameter lines
|
|
261
|
+
that appear before any section header land under ``params[""]``. A
|
|
262
|
+
record with no operation line still yields its timestamp, with
|
|
263
|
+
``operation == ""``. Returns ``[]`` when ``text`` holds no timestamp
|
|
264
|
+
headers at all (e.g. ``""``).
|
|
265
|
+
"""
|
|
266
|
+
records: list[dict[str, object]] = []
|
|
267
|
+
headers = list(_LOG_HEADER.finditer(text))
|
|
268
|
+
for i, m in enumerate(headers):
|
|
269
|
+
body_end = headers[i + 1].start() if i + 1 < len(headers) else len(text)
|
|
270
|
+
operation = ""
|
|
271
|
+
params: dict[str, dict[str, str]] = {}
|
|
272
|
+
extra: list[str] = []
|
|
273
|
+
section = ""
|
|
274
|
+
is_first_line = True
|
|
275
|
+
for line in text[m.end() : body_end].splitlines():
|
|
276
|
+
stripped = line.strip()
|
|
277
|
+
if not stripped:
|
|
278
|
+
continue
|
|
279
|
+
if is_first_line:
|
|
280
|
+
is_first_line = False
|
|
281
|
+
op_m = _OPERATION_LINE.match(stripped)
|
|
282
|
+
if op_m:
|
|
283
|
+
operation = op_m.group(1)
|
|
284
|
+
continue
|
|
285
|
+
# no operation line present -- fall through, reprocess below
|
|
286
|
+
param_m = _PARAM_LINE.match(stripped)
|
|
287
|
+
if param_m:
|
|
288
|
+
params.setdefault(section, {})[param_m.group(1)] = param_m.group(2).strip()
|
|
289
|
+
elif _SECTION_LINE.match(stripped):
|
|
290
|
+
section = stripped
|
|
291
|
+
params.setdefault(section, {})
|
|
292
|
+
else:
|
|
293
|
+
extra.append(stripped)
|
|
294
|
+
record: dict[str, object] = {
|
|
295
|
+
"timestamp": m.group(1),
|
|
296
|
+
"operation": operation,
|
|
297
|
+
"params": params,
|
|
298
|
+
}
|
|
299
|
+
if extra:
|
|
300
|
+
record["extra"] = extra
|
|
301
|
+
records.append(record)
|
|
302
|
+
return records
|