refactor(bw-report): parse user notes by POSITION, not by label
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.
This commit is contained in:
@@ -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 `<label> : <value>` lines are assigned to
|
||||
# the 4 canonical operator-supplied slots by POSITION (project,
|
||||
# client, operator, sensor_location) regardless of what the
|
||||
# operator named the labels in BW's Compliance Setup → Notes tab.
|
||||
in_user_notes_block = False
|
||||
user_note_position = 0
|
||||
|
||||
while i < n:
|
||||
raw_line = lines[i]
|
||||
i += 1
|
||||
@@ -399,18 +386,28 @@ def parse_report(text: Union[str, bytes], *, parse_samples: bool = False) -> BwA
|
||||
elif key == "Battery Level": report.battery_volts = _parse_number(value)
|
||||
elif key == "Calibration":
|
||||
report.calibration_date, report.calibration_by = _parse_calibration(value)
|
||||
elif key == "Units": report.units = value
|
||||
elif key == "Units":
|
||||
report.units = value
|
||||
# Entering the user-notes block. Next ~4 lines until
|
||||
# "Geo Range :" are the operator-supplied notes.
|
||||
in_user_notes_block = True
|
||||
user_note_position = 0
|
||||
|
||||
# Operator-supplied labels (Project / Client / User Name /
|
||||
# Seis Loc) — BW writes these with assorted spellings across
|
||||
# firmware versions and recording modes (e.g. "Seis Loc:" on
|
||||
# waveform exports vs "Seis. Location" on histogram exports).
|
||||
# The label normaliser absorbs all known variants; see
|
||||
# `_OPERATOR_LABEL_MAP` above for the dispatch table.
|
||||
elif (slot := _OPERATOR_LABEL_MAP.get(_normalise_label_for_lookup(key))):
|
||||
elif key == "Geo Range":
|
||||
# Exiting the user-notes block.
|
||||
in_user_notes_block = False
|
||||
report.geo_range_ips = _parse_number(value)
|
||||
|
||||
# User-notes block: assign by position (operator may have
|
||||
# renamed the labels, so we don't trust them). Preserve the
|
||||
# original labels in `user_note_labels` for downstream UIs
|
||||
# (terra-view) that want to display them as the operator
|
||||
# named them.
|
||||
elif in_user_notes_block and user_note_position < len(_USER_NOTE_SLOTS):
|
||||
slot = _USER_NOTE_SLOTS[user_note_position]
|
||||
setattr(report, slot, value)
|
||||
|
||||
elif key == "Geo Range": report.geo_range_ips = _parse_number(value)
|
||||
report.user_note_labels[slot] = key
|
||||
user_note_position += 1
|
||||
|
||||
# ── Per-channel stats ────────────────────────────────────────────────
|
||||
# All match the pattern "{Channel} <stat-name>"
|
||||
|
||||
Reference in New Issue
Block a user