v0.20.0 -- Full s3 event parse and PDF creation. #28

Merged
serversdown merged 46 commits from dev into main 2026-05-28 17:54:34 -04:00
2 changed files with 46 additions and 14 deletions
Showing only changes of commit 6381dcb312 - Show all commits
+12 -1
View File
@@ -2,10 +2,21 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# tzdata is required for the TZ env var to take effect (python:slim
# omits the timezone database). Without it, datetime.now() / logging
# / matplotlib all stay in UTC regardless of TZ. Default zone gets
# set further down via ENV; users override per-deployment via the
# `TZ` env var in docker-compose.
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \ apt-get install -y --no-install-recommends curl tzdata && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Default display timezone — applied to server logs, datetime.now(),
# matplotlib rendered timestamps, and any naïve-vs-aware datetime
# conversions in the PDF renderer. Override via TZ env var in
# docker-compose; storage in the DB is always UTC regardless.
ENV TZ=America/New_York
COPY pyproject.toml requirements.txt ./ COPY pyproject.toml requirements.txt ./
COPY minimateplus ./minimateplus COPY minimateplus ./minimateplus
COPY micromate ./micromate COPY micromate ./micromate
+34 -13
View File
@@ -348,9 +348,11 @@ def render_event_report_pdf(rd: ReportData) -> bytes:
# Page footer (common to both layouts) — Created date + event id. # Page footer (common to both layouts) — Created date + event id.
# Pushed to the very page bottom so it doesn't collide with the # Pushed to the very page bottom so it doesn't collide with the
# waveform footer scale / trigger legend lines just above. # waveform footer scale / trigger legend lines just above.
# Convert UTC server_received_at to local for display.
created_local = _fmt_iso_to_bw(rd.server_received_at) if rd.server_received_at else ""
fig.text( fig.text(
0.07, 0.005, 0.07, 0.005,
f"Created: {rd.server_received_at or ''} • seismo-relay", f"Created: {created_local} • seismo-relay",
fontsize=6, color="#888", ha="left", fontsize=6, color="#888", ha="left",
) )
fig.text( fig.text(
@@ -419,31 +421,50 @@ def _render_histogram_layout(fig, rd: ReportData) -> None:
_draw_histogram_subplot(fig, gs[3], rd) _draw_histogram_subplot(fig, gs[3], rd)
def _to_display_local(iso: str):
"""Parse an ISO timestamp and return a datetime in the system's local
timezone (set by the TZ env var, default America/New_York via the
Dockerfile).
Behaviour:
- "...Z" or "...+HH:MM" suffix → tz-aware UTC → converted to local
- Naïve "YYYY-MM-DDTHH:MM:SS" (no tz) → returned as-is. This
matches the convention used elsewhere in seismo-relay: BW's
recorded-at timestamps are naïve and ALREADY in the unit's
local clock; we don't second-guess them.
"""
import datetime as _dt
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
if dt.tzinfo is not None:
# Convert from UTC (or other tz) → local per the TZ env var.
# astimezone() without arg uses the system timezone.
dt = dt.astimezone()
return dt
def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]: def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]:
"""Convert a ISO-8601 timestamp like '2026-05-16T22:30:37' to BW's """Convert an ISO-8601 timestamp to BW's display format
display format '22:30:37 May 16, 2026'. Returns input unchanged if '22:30:37 May 16, 2026'. UTC inputs (with Z suffix) are
it doesn't look like ISO.""" converted to the system's local timezone first; naïve inputs
are formatted as-is. Returns input unchanged on parse failure."""
if not iso or "T" not in iso: if not iso or "T" not in iso:
return iso return iso
try: try:
import datetime as _dt return _to_display_local(iso).strftime("%H:%M:%S %B %d, %Y").replace(" 0", " ")
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
return dt.strftime("%H:%M:%S %B %d, %Y").replace(" 0", " ")
except Exception: except Exception:
return iso return iso
def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]: def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]:
"""Split an ISO timestamp into BW-formatted ("May 27 /26", "06:06:14") """Split an ISO timestamp into BW-formatted ('May 27 /26', '06:06:14')
date+time strings. Used for the histogram stats table where the date+time strings. Used for the histogram stats table where the
Date and Time rows are presented separately. Returns (None, None) Date and Time rows are presented separately. UTC inputs are
if the input isn't a valid ISO datetime.""" converted to local time first. Returns (None, None) on parse failure."""
if not iso: if not iso:
return (None, None) return (None, None)
try: try:
import datetime as _dt dt = _to_display_local(iso)
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00")) # BW format: 'May 27 /26' (3-letter month + 2-digit year)
# BW format: "May 27 /26" (3-letter month + 2-digit year)
date_str = dt.strftime("%b %d /%y").replace(" 0", " ") date_str = dt.strftime("%b %d /%y").replace(" 0", " ")
time_str = dt.strftime("%H:%M:%S") time_str = dt.strftime("%H:%M:%S")
return (date_str, time_str) return (date_str, time_str)