From 916e6fcabbac5e79526be1cef7d855b30187ff72 Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 2 Jul 2026 06:30:11 +0000 Subject: [PATCH] feat(db): zip recent events' waveform files --- sfm/db_snapshot.py | 26 ++++++++++++++++++++++++++ tests/test_db_snapshot.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/sfm/db_snapshot.py b/sfm/db_snapshot.py index 0bd2d68..45bd3d8 100644 --- a/sfm/db_snapshot.py +++ b/sfm/db_snapshot.py @@ -6,6 +6,7 @@ unit-testable without HTTP. """ from __future__ import annotations import sqlite3 +import zipfile from pathlib import Path @@ -22,3 +23,28 @@ def snapshot_db(src_path: Path, dest_path: Path) -> Path: src.close() dest.close() return Path(dest_path) + + +def build_recent_waveforms_zip(db, store, n: int, dest_path: Path) -> dict: + """Zip every on-disk file (event file + .h5/.a5.pkl/.sfm.json siblings) for + the n most-recent events, arcname '/'. Events with no + resolvable files on disk are skipped. Returns {'events': int, 'files': int}.""" + rows = db.query_events(limit=n, offset=0) + events = 0 + files = 0 + with zipfile.ZipFile(dest_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for row in rows: + serial = row.get("serial") + fn = row.get("blastware_filename") + if not serial or not fn: + continue + serial_dir = store.root / serial + matched = sorted(serial_dir.glob(f"{fn}*")) if serial_dir.is_dir() else [] + matched = [p for p in matched if p.is_file()] + if not matched: + continue + events += 1 + for p in matched: + zf.write(p, arcname=f"{serial}/{p.name}") + files += 1 + return {"events": events, "files": files} diff --git a/tests/test_db_snapshot.py b/tests/test_db_snapshot.py index 32cacaa..0a5f39b 100644 --- a/tests/test_db_snapshot.py +++ b/tests/test_db_snapshot.py @@ -45,3 +45,37 @@ def test_snapshot_db_produces_queryable_copy(tmp_path): n = conn.execute("SELECT COUNT(*) FROM t").fetchone()[0] conn.close() assert n == 5 + + +def test_recent_waveforms_zip_bundles_all_per_event_files(tmp_path): + import zipfile + from sfm.database import SeismoDb + from sfm.waveform_store import WaveformStore + from sfm.db_snapshot import build_recent_waveforms_zip + + db = SeismoDb(tmp_path / "seismo_relay.db") # creates schema on init + store = WaveformStore(tmp_path / "waveforms") + + def add(serial, fn, ts): + d = store.root / serial + d.mkdir(parents=True, exist_ok=True) + for suffix in ("", ".h5", ".a5.pkl", ".sfm.json"): + (d / f"{fn}{suffix}").write_bytes(b"data") + _seed_event(db.db_path, serial, fn, ts) + + add("BE111", "AAAA1111.OE0H", "2026-06-01T00:00:00") + add("BE111", "BBBB2222.OE0H", "2026-06-02T00:00:00") # newest + + dest = tmp_path / "wf.zip" + stats = build_recent_waveforms_zip(db, store, n=1, dest_path=dest) + + assert stats["events"] == 1 + assert stats["files"] == 4 + with zipfile.ZipFile(dest) as zf: + names = set(zf.namelist()) + assert names == { + "BE111/BBBB2222.OE0H", + "BE111/BBBB2222.OE0H.h5", + "BE111/BBBB2222.OE0H.a5.pkl", + "BE111/BBBB2222.OE0H.sfm.json", + }