From a032fa5451e00f317bad0365cf51f984f9fab768 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 10 May 2026 22:28:31 +0000 Subject: [PATCH] refactor(bw-report): parse user notes by POSITION, not by label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four operator-supplied note fields in BW's Compliance Setup → Notes tab (Project / Client / User Name / Seis Loc) have USER-EDITABLE LABELS — an operator can rename them in BW's UI to "Building:", "Site Address:", "Inspector:", or anything else, and the ASCII export writes those literal labels verbatim. The previous label-normalisation map approach (just added in commit 6a7e8c6) was fragile: it could only match label spellings we'd enumerated in advance. An operator using "Site:" instead of "Seis Loc:" would have their sensor location silently dropped. What IS reliable: BW always writes the 4 user-notes lines contiguously, in the same order, between the "Units :" line and the "Geo Range :" line of the export. So parse them by POSITION: position 1 → project position 2 → client position 3 → operator position 4 → sensor_location The original labels BW wrote are preserved in a new `BwAsciiReport.user_note_labels` dict (canonical slot → literal label string) so terra-view can render them as the operator named them. Removes the `_OPERATOR_LABEL_MAP` / `_normalise_label_for_lookup` helpers and the elif-by-normalised-label branch in `parse_report`. Replaces with a small state machine that flips on the "Units" line and flips off on the "Geo Range" line. Tests: - Default-label fixtures (waveform + histogram) still populate correctly, with operator's labels captured. - Synthetic custom-labelled exports ("Building:" / "Site Address:" / etc.) populate the right slots by position. - Histogram-specific "Seis. Location:" works. - Lines outside the Units→Geo Range range are ignored even if they look like user notes (defensive against malformed exports). - Partial blocks (fewer than 4 lines) leave later slots None. - Extra lines beyond 4 are dropped (5th slot doesn't exist). 26 tests in test_bw_ascii_report.py (was 33; net drop reflects parametrised label tests collapsed into 6 focused position tests). Full SFM suite: 62 passed, 44 skipped. Pairs with series3-watcher v1.5.0 which fixes the filename pairing so the report reaches this parser in the first place. --- minimateplus/bw_ascii_report.py | 125 ++++++++++++------------- tests/test_bw_ascii_report.py | 159 ++++++++++++++++++++++++-------- 2 files changed, 181 insertions(+), 103 deletions(-) diff --git a/minimateplus/bw_ascii_report.py b/minimateplus/bw_ascii_report.py index 7451d35..a3aee4b 100644 --- a/minimateplus/bw_ascii_report.py +++ b/minimateplus/bw_ascii_report.py @@ -115,10 +115,24 @@ class BwAsciiReport: units: Optional[str] = None # e.g. "in/s and dB(L)" # ── Operator-supplied metadata ────────────────────────────────────────── - project: Optional[str] = None - client: Optional[str] = None - operator: Optional[str] = None # User Name - sensor_location: Optional[str] = None # Seis Loc + # Parsed by POSITION from the 4-line "User Notes" block BW writes + # between the `Units :` and `Geo Range :` lines. Position-based so + # the values populate correctly even when an operator renames the + # labels in Blastware's Compliance Setup → Notes tab (the 4 labels + # are user-editable, e.g. "Seis Loc:" → "Building:" → "Site Address:"). + # The original labels BW wrote are preserved in `user_note_labels` + # so terra-view can render them as the operator named them. + project: Optional[str] = None # position 1 (BW default label "Project:") + client: Optional[str] = None # position 2 (BW default label "Client:") + operator: Optional[str] = None # position 3 (BW default label "User Name:") + sensor_location: Optional[str] = None # position 4 (BW default label "Seis Loc:") + + # Maps canonical slot name → the literal label BW wrote in the ASCII + # export. Empty if the User Notes block wasn't present. Example + # when the operator renamed slot 4 to "Building:": + # {"project": "Project:", "client": "Client:", + # "operator": "User Name:", "sensor_location": "Building:"} + user_note_labels: Dict[str, str] = field(default_factory=dict) # ── Geo channel scaling ───────────────────────────────────────────────── geo_range_ips: Optional[float] = None # 10.000 / 1.250 @@ -265,59 +279,23 @@ def _parse_monitor_ts(s: str) -> Optional[datetime.datetime]: return None -# ── Operator-field label normalisation ────────────────────────────────────── +# ── User-notes positional slot map ────────────────────────────────────────── # -# BW has used different label spellings across versions and recording -# modes for the same operator-supplied fields: +# Blastware's Compliance Setup → Notes tab shows four operator-supplied +# fields whose LABELS the operator can rename (see screenshot in +# project archive). Defaults are "Project:" / "Client:" / +# "User Name:" / "Seis Loc:", but an operator using a different +# convention can rename them to anything ("Building:", "Site:", +# "Address:", etc.). The ASCII export reflects whatever the operator +# typed, so label-based matching is fragile. # -# project: "Project:" / "Project" -# client: "Client:" / "Client" -# operator: "User Name:" / "User Name" -# sensor_location: "Seis Loc:" / "Seis. Location" / "Seis Location" -# / "Sensor Location" -# -# Per user feedback ("the tags themselves dont matter a ton, what -# matters is the field"), we normalise labels at lookup time so the -# value-extraction works regardless of which spelling BW happens to -# emit on a given machine. -# -# To add a new variant: edit `_OPERATOR_LABEL_MAP` — single source of -# truth. Keys are normalised forms (lowercase, trailing colon and -# period stripped, internal whitespace collapsed); values are -# attribute names on `BwAsciiReport`. +# What IS reliable: BW always writes the 4 user-notes lines in the +# same order, contiguously between the `Units :` line and the +# `Geo Range :` line. We parse them by POSITION and preserve the +# operator's labels in `report.user_note_labels` so terra-view can +# render them as the operator intended. -_OPERATOR_LABEL_MAP = { - # project - "project": "project", - # client - "client": "client", - # operator - "user name": "operator", - # sensor location — most variants of "Seis*" + "Sensor Location" - "seis loc": "sensor_location", - "seis. loc": "sensor_location", - "seis. location": "sensor_location", - "seis location": "sensor_location", - "sensor location": "sensor_location", -} - - -def _normalise_label_for_lookup(key: str) -> str: - """Normalise a label for the operator-field lookup. - - Strips a trailing colon and/or period, collapses internal - whitespace runs, and lowercases. So all of: - - "Seis Loc:" - "Seis. Location" - "seis location" - "Sensor Location" - - map to canonical forms in `_OPERATOR_LABEL_MAP`. - """ - s = key.strip().rstrip(":").rstrip(".").strip() - s = _KEY_NORMALISE_RE.sub(" ", s) - return s.lower() +_USER_NOTE_SLOTS = ("project", "client", "operator", "sensor_location") # ───────────────────────────────────────────────────────────────────────────── @@ -349,6 +327,15 @@ def parse_report(text: Union[str, bytes], *, parse_samples: bool = False) -> BwA event_time_str: Optional[str] = None event_date: Optional[datetime.date] = None + # User-notes block detection. We enter the block after parsing + # the "Units :" line and exit on the "Geo Range :" line. Inside, + # the first 4 unmatched `