Files
PolyWeather/src/database/runtime_state.py
T

1174 lines
45 KiB
Python

from __future__ import annotations
import json
import os
import sqlite3
import threading
import time
from pathlib import Path
from typing import Any, Dict, List, Optional
from loguru import logger
from src.database.db_manager import DBManager
STATE_STORAGE_FILE = "file"
STATE_STORAGE_DUAL = "dual"
STATE_STORAGE_SQLITE = "sqlite"
DEFAULT_STATE_STORAGE_MODE = STATE_STORAGE_SQLITE
VALID_STATE_STORAGE_MODES = {
STATE_STORAGE_FILE,
STATE_STORAGE_SQLITE,
}
_LOGGED_MODES: set[str] = set()
def get_state_storage_mode() -> str:
raw = str(os.getenv("POLYWEATHER_STATE_STORAGE_MODE") or DEFAULT_STATE_STORAGE_MODE).strip().lower()
if raw == STATE_STORAGE_DUAL:
logger.warning(
f"POLYWEATHER_STATE_STORAGE_MODE={STATE_STORAGE_DUAL!r} is deprecated, normalize to {STATE_STORAGE_SQLITE}"
)
raw = STATE_STORAGE_SQLITE
if raw not in VALID_STATE_STORAGE_MODES:
logger.warning(
f"invalid POLYWEATHER_STATE_STORAGE_MODE={raw!r}, fallback to {DEFAULT_STATE_STORAGE_MODE}"
)
raw = DEFAULT_STATE_STORAGE_MODE
if raw not in _LOGGED_MODES:
logger.info(f"runtime state storage mode={raw}")
_LOGGED_MODES.add(raw)
return raw
class RuntimeStateDB:
_instance: Optional["RuntimeStateDB"] = None
_instance_lock = threading.Lock()
def __init__(self, db_path: Optional[str] = None):
self.db_path = DBManager(db_path).db_path
self._init_tables()
@classmethod
def instance(cls) -> "RuntimeStateDB":
with cls._instance_lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
def _init_tables(self) -> None:
with self.connect() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS daily_records_store (
city TEXT NOT NULL,
target_date TEXT NOT NULL,
actual_high REAL,
deb_prediction REAL,
mu REAL,
updated_at REAL NOT NULL,
payload_json TEXT NOT NULL,
PRIMARY KEY (city, target_date)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS truth_records_store (
city TEXT NOT NULL,
target_date TEXT NOT NULL,
actual_high REAL NOT NULL,
settlement_source TEXT,
settlement_station_code TEXT,
settlement_station_label TEXT,
truth_version TEXT,
updated_by TEXT,
updated_at REAL NOT NULL,
source_payload_json TEXT,
is_final INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (city, target_date)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS truth_revisions_store (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
target_date TEXT NOT NULL,
previous_actual_high REAL,
next_actual_high REAL NOT NULL,
previous_source TEXT,
next_source TEXT,
truth_version TEXT,
updated_by TEXT,
updated_at REAL NOT NULL,
reason TEXT,
payload_json TEXT
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_truth_records_city_date ON truth_records_store(city, target_date)"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_truth_revisions_city_date ON truth_revisions_store(city, target_date, id DESC)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS telegram_alert_last_by_city (
city TEXT PRIMARY KEY,
signature TEXT,
trigger_key TEXT,
severity TEXT,
ts INTEGER,
active INTEGER DEFAULT 0,
cleared_ts INTEGER,
evidence_json TEXT
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS telegram_alert_signature_state (
signature TEXT PRIMARY KEY,
ts INTEGER NOT NULL
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS probability_training_snapshots_store (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
target_date TEXT NOT NULL,
timestamp TEXT NOT NULL,
raw_mu REAL,
raw_sigma REAL,
max_so_far REAL,
peak_status TEXT,
probability_mode TEXT,
legacy_top_bucket INTEGER,
shadow_top_bucket INTEGER,
payload_json TEXT NOT NULL
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_probability_snapshot_city_date ON probability_training_snapshots_store(city, target_date, id DESC)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS training_feature_records_store (
city TEXT NOT NULL,
target_date TEXT NOT NULL,
updated_at REAL NOT NULL,
payload_json TEXT NOT NULL,
PRIMARY KEY (city, target_date)
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_training_feature_records_city_date ON training_feature_records_store(city, target_date)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS open_meteo_cache_store (
source_kind TEXT NOT NULL,
cache_key TEXT NOT NULL,
updated_at REAL NOT NULL,
expires_at REAL,
payload_json TEXT NOT NULL,
PRIMARY KEY (source_kind, cache_key)
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_open_meteo_cache_expires ON open_meteo_cache_store(source_kind, expires_at)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS official_intraday_observations_store (
source_code TEXT NOT NULL,
station_code TEXT NOT NULL,
target_date TEXT NOT NULL,
observation_time TEXT NOT NULL,
value REAL NOT NULL,
payload_json TEXT,
PRIMARY KEY (source_code, station_code, observation_time)
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_official_intraday_obs_station_date ON official_intraday_observations_store(source_code, station_code, target_date, observation_time)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS intraday_path_snapshots_store (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
target_date TEXT NOT NULL,
snapshot_time TEXT NOT NULL,
local_time TEXT,
deb_prediction REAL,
forecast_today_high REAL,
current_temp REAL,
max_so_far REAL,
observation_count INTEGER NOT NULL DEFAULT 0,
payload_json TEXT NOT NULL
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_intraday_path_snapshots_city_date ON intraday_path_snapshots_store(city, target_date, id DESC)"
)
conn.commit()
class DailyRecordRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def load_all(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
out: Dict[str, Dict[str, Dict[str, Any]]] = {}
with self.db.connect() as conn:
rows = conn.execute(
"SELECT city, target_date, payload_json FROM daily_records_store ORDER BY city, target_date"
).fetchall()
for row in rows:
try:
payload = json.loads(row["payload_json"])
except Exception:
continue
city = str(row["city"])
date_str = str(row["target_date"])
out.setdefault(city, {})[date_str] = payload
return out
def load_recent_settled_rows(
self,
before_date: str,
per_city_limit: int,
) -> Dict[str, List[Dict[str, Any]]]:
out: Dict[str, List[Dict[str, Any]]] = {}
limit = max(int(per_city_limit or 0), 1)
query = """
SELECT city, target_date, actual_high, deb_prediction
FROM (
SELECT
city,
target_date,
actual_high,
deb_prediction,
ROW_NUMBER() OVER (
PARTITION BY city
ORDER BY target_date DESC
) AS row_num
FROM daily_records_store
WHERE
target_date < ?
AND actual_high IS NOT NULL
AND deb_prediction IS NOT NULL
)
WHERE row_num <= ?
ORDER BY city, target_date DESC
"""
fallback_query = """
SELECT city, target_date, actual_high, deb_prediction
FROM daily_records_store
WHERE
target_date < ?
AND actual_high IS NOT NULL
AND deb_prediction IS NOT NULL
ORDER BY city, target_date DESC
"""
try:
with self.db.connect() as conn:
rows = conn.execute(query, (before_date, limit)).fetchall()
except sqlite3.OperationalError:
with self.db.connect() as conn:
rows = conn.execute(fallback_query, (before_date,)).fetchall()
counts: Dict[str, int] = {}
for row in rows:
city = str(row["city"] or "").strip().lower()
if not city:
continue
current_count = counts.get(city, 0)
if current_count >= limit:
continue
out.setdefault(city, []).append(
{
"target_date": str(row["target_date"]),
"actual_high": row["actual_high"],
"deb_prediction": row["deb_prediction"],
}
)
counts[city] = current_count + 1
return out
def upsert_record(self, city: str, target_date: str, record: Dict[str, Any]) -> None:
payload_json = json.dumps(record, ensure_ascii=False)
updated_at = time.time()
with self.db.connect() as conn:
conn.execute(
"""
INSERT INTO daily_records_store (
city, target_date, actual_high, deb_prediction, mu, updated_at, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(city, target_date) DO UPDATE SET
actual_high = excluded.actual_high,
deb_prediction = excluded.deb_prediction,
mu = excluded.mu,
updated_at = excluded.updated_at,
payload_json = excluded.payload_json
""",
(
city,
target_date,
record.get("actual_high"),
record.get("deb_prediction"),
record.get("mu"),
updated_at,
payload_json,
),
)
conn.commit()
def replace_all(self, data: Dict[str, Dict[str, Dict[str, Any]]]) -> int:
count = 0
with self.db.connect() as conn:
conn.execute("DELETE FROM daily_records_store")
for city, city_rows in (data or {}).items():
if not isinstance(city_rows, dict):
continue
for target_date, record in city_rows.items():
payload_json = json.dumps(record, ensure_ascii=False)
conn.execute(
"""
INSERT INTO daily_records_store (
city, target_date, actual_high, deb_prediction, mu, updated_at, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
city,
target_date,
record.get("actual_high"),
record.get("deb_prediction"),
record.get("mu"),
time.time(),
payload_json,
),
)
count += 1
conn.commit()
return count
def delete_older_than(self, cutoff_date: str) -> int:
with self.db.connect() as conn:
cur = conn.execute(
"DELETE FROM daily_records_store WHERE target_date < ?",
(cutoff_date,),
)
conn.commit()
return int(cur.rowcount or 0)
class TruthRecordRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def load_all(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
out: Dict[str, Dict[str, Dict[str, Any]]] = {}
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT city, target_date, actual_high, settlement_source, settlement_station_code,
settlement_station_label, truth_version, updated_by, updated_at,
source_payload_json, is_final
FROM truth_records_store
ORDER BY city, target_date
"""
).fetchall()
for row in rows:
payload: Dict[str, Any] = {
"actual_high": float(row["actual_high"]),
"settlement_source": row["settlement_source"],
"settlement_station_code": row["settlement_station_code"],
"settlement_station_label": row["settlement_station_label"],
"truth_version": row["truth_version"],
"updated_by": row["updated_by"],
"truth_updated_at": float(row["updated_at"]),
"is_final": bool(row["is_final"]),
}
if row["source_payload_json"]:
try:
payload["source_payload"] = json.loads(row["source_payload_json"])
except Exception:
pass
out.setdefault(str(row["city"]), {})[str(row["target_date"])] = payload
return out
def get_record(self, city: str, target_date: str) -> Optional[Dict[str, Any]]:
with self.db.connect() as conn:
row = conn.execute(
"""
SELECT actual_high, settlement_source, settlement_station_code,
settlement_station_label, truth_version, updated_by, updated_at,
source_payload_json, is_final
FROM truth_records_store
WHERE city = ? AND target_date = ?
""",
(city, target_date),
).fetchone()
if not row:
return None
payload: Dict[str, Any] = {
"actual_high": float(row["actual_high"]),
"settlement_source": row["settlement_source"],
"settlement_station_code": row["settlement_station_code"],
"settlement_station_label": row["settlement_station_label"],
"truth_version": row["truth_version"],
"updated_by": row["updated_by"],
"truth_updated_at": float(row["updated_at"]),
"is_final": bool(row["is_final"]),
}
if row["source_payload_json"]:
try:
payload["source_payload"] = json.loads(row["source_payload_json"])
except Exception:
pass
return payload
def load_city(self, city: str) -> Dict[str, Dict[str, Any]]:
out: Dict[str, Dict[str, Any]] = {}
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT target_date, actual_high, settlement_source, settlement_station_code,
settlement_station_label, truth_version, updated_by, updated_at,
source_payload_json, is_final
FROM truth_records_store
WHERE city = ?
ORDER BY target_date
""",
(city,),
).fetchall()
for row in rows:
payload: Dict[str, Any] = {
"actual_high": float(row["actual_high"]),
"settlement_source": row["settlement_source"],
"settlement_station_code": row["settlement_station_code"],
"settlement_station_label": row["settlement_station_label"],
"truth_version": row["truth_version"],
"updated_by": row["updated_by"],
"truth_updated_at": float(row["updated_at"]),
"is_final": bool(row["is_final"]),
}
if row["source_payload_json"]:
try:
payload["source_payload"] = json.loads(row["source_payload_json"])
except Exception:
pass
out[str(row["target_date"])] = payload
return out
def upsert_truth(
self,
*,
city: str,
target_date: str,
actual_high: float,
settlement_source: Optional[str],
settlement_station_code: Optional[str],
settlement_station_label: Optional[str],
truth_version: str,
updated_by: str,
source_payload: Optional[Dict[str, Any]] = None,
is_final: bool = True,
reason: Optional[str] = None,
) -> bool:
updated_at = time.time()
payload_json = (
json.dumps(source_payload, ensure_ascii=False) if source_payload is not None else None
)
with self.db.connect() as conn:
current = conn.execute(
"""
SELECT actual_high, settlement_source, source_payload_json
FROM truth_records_store
WHERE city = ? AND target_date = ?
""",
(city, target_date),
).fetchone()
changed = True
if current:
prev_actual = float(current["actual_high"])
prev_source = str(current["settlement_source"] or "")
next_source = str(settlement_source or "")
changed = (
abs(prev_actual - float(actual_high)) >= 0.0001
or prev_source != next_source
or str(current["source_payload_json"] or "") != str(payload_json or "")
)
if changed:
conn.execute(
"""
INSERT INTO truth_revisions_store (
city, target_date, previous_actual_high, next_actual_high,
previous_source, next_source, truth_version, updated_by,
updated_at, reason, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
city,
target_date,
prev_actual,
float(actual_high),
prev_source or None,
next_source or None,
truth_version,
updated_by,
updated_at,
reason,
payload_json,
),
)
conn.execute(
"""
INSERT INTO truth_records_store (
city, target_date, actual_high, settlement_source,
settlement_station_code, settlement_station_label, truth_version,
updated_by, updated_at, source_payload_json, is_final
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(city, target_date) DO UPDATE SET
actual_high = excluded.actual_high,
settlement_source = excluded.settlement_source,
settlement_station_code = excluded.settlement_station_code,
settlement_station_label = excluded.settlement_station_label,
truth_version = excluded.truth_version,
updated_by = excluded.updated_by,
updated_at = excluded.updated_at,
source_payload_json = excluded.source_payload_json,
is_final = excluded.is_final
""",
(
city,
target_date,
float(actual_high),
settlement_source,
settlement_station_code,
settlement_station_label,
truth_version,
updated_by,
updated_at,
payload_json,
1 if is_final else 0,
),
)
conn.commit()
return changed
def replace_all(self, rows: Dict[str, Dict[str, Dict[str, Any]]]) -> int:
count = 0
with self.db.connect() as conn:
conn.execute("DELETE FROM truth_records_store")
conn.execute("DELETE FROM truth_revisions_store")
for city, city_rows in (rows or {}).items():
if not isinstance(city_rows, dict):
continue
for target_date, record in city_rows.items():
if not isinstance(record, dict):
continue
actual_high = record.get("actual_high")
if actual_high is None:
continue
payload_json = (
json.dumps(record.get("source_payload"), ensure_ascii=False)
if record.get("source_payload") is not None
else None
)
conn.execute(
"""
INSERT INTO truth_records_store (
city, target_date, actual_high, settlement_source,
settlement_station_code, settlement_station_label, truth_version,
updated_by, updated_at, source_payload_json, is_final
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
city,
target_date,
float(actual_high),
record.get("settlement_source"),
record.get("settlement_station_code"),
record.get("settlement_station_label"),
record.get("truth_version") or "v1",
record.get("updated_by") or "replace_all",
float(record.get("truth_updated_at") or time.time()),
payload_json,
1 if record.get("is_final", True) else 0,
),
)
count += 1
conn.commit()
return count
class TruthRevisionRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def load_revisions(self, city: str, target_date: str) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT previous_actual_high, next_actual_high, previous_source, next_source,
truth_version, updated_by, updated_at, reason, payload_json
FROM truth_revisions_store
WHERE city = ? AND target_date = ?
ORDER BY id ASC
""",
(city, target_date),
).fetchall()
out: List[Dict[str, Any]] = []
for row in rows:
entry: Dict[str, Any] = {
"previous_actual_high": row["previous_actual_high"],
"next_actual_high": row["next_actual_high"],
"previous_source": row["previous_source"],
"next_source": row["next_source"],
"truth_version": row["truth_version"],
"updated_by": row["updated_by"],
"updated_at": float(row["updated_at"]),
"reason": row["reason"],
}
if row["payload_json"]:
try:
entry["payload"] = json.loads(row["payload_json"])
except Exception:
pass
out.append(entry)
return out
class TelegramAlertStateRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def load_state(self) -> Dict[str, Any]:
state = {"last_by_city": {}, "by_signature": {}}
with self.db.connect() as conn:
city_rows = conn.execute(
"SELECT city, signature, trigger_key, severity, ts, active, cleared_ts, evidence_json FROM telegram_alert_last_by_city"
).fetchall()
sig_rows = conn.execute(
"SELECT signature, ts FROM telegram_alert_signature_state"
).fetchall()
for row in city_rows:
entry = {
"signature": row["signature"],
"trigger_key": row["trigger_key"],
"severity": row["severity"],
"ts": row["ts"],
"active": bool(row["active"]),
}
if row["cleared_ts"] is not None:
entry["cleared_ts"] = row["cleared_ts"]
if row["evidence_json"]:
try:
entry["evidence"] = json.loads(row["evidence_json"])
except Exception:
pass
state["last_by_city"][str(row["city"])] = entry
for row in sig_rows:
state["by_signature"][str(row["signature"])] = int(row["ts"] or 0)
return state
def save_state(self, state: Dict[str, Any]) -> None:
last_by_city = state.get("last_by_city") or {}
by_signature = state.get("by_signature") or {}
with self.db.connect() as conn:
conn.execute("DELETE FROM telegram_alert_last_by_city")
conn.execute("DELETE FROM telegram_alert_signature_state")
for city, row in last_by_city.items():
if not isinstance(row, dict):
continue
conn.execute(
"""
INSERT INTO telegram_alert_last_by_city (
city, signature, trigger_key, severity, ts, active, cleared_ts, evidence_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
city,
row.get("signature"),
row.get("trigger_key"),
row.get("severity"),
int(row.get("ts") or 0),
1 if row.get("active") else 0,
row.get("cleared_ts"),
json.dumps(row.get("evidence"), ensure_ascii=False)
if row.get("evidence") is not None
else None,
),
)
for signature, ts in by_signature.items():
conn.execute(
"INSERT INTO telegram_alert_signature_state (signature, ts) VALUES (?, ?)",
(signature, int(ts or 0)),
)
conn.commit()
def replace_from_state(self, state: Dict[str, Any]) -> int:
self.save_state(state)
return len((state.get("last_by_city") or {})) + len((state.get("by_signature") or {}))
class ProbabilitySnapshotRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def append_snapshot(self, payload: Dict[str, Any]) -> None:
legacy_top = _top_bucket(payload.get("prob_snapshot"))
shadow_top = _top_bucket(payload.get("shadow_prob_snapshot"))
with self.db.connect() as conn:
conn.execute(
"""
INSERT INTO probability_training_snapshots_store (
city, target_date, timestamp, raw_mu, raw_sigma, max_so_far,
peak_status, probability_mode, legacy_top_bucket, shadow_top_bucket, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payload.get("city"),
payload.get("date"),
payload.get("timestamp"),
payload.get("raw_mu"),
payload.get("raw_sigma"),
payload.get("max_so_far"),
payload.get("peak_status"),
payload.get("probability_mode"),
legacy_top,
shadow_top,
json.dumps(payload, ensure_ascii=False),
),
)
conn.commit()
def load_recent_rows(self, city: str, target_date: str, limit: int) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT payload_json
FROM probability_training_snapshots_store
WHERE city = ? AND target_date = ?
ORDER BY id DESC
LIMIT ?
""",
(city, target_date, int(limit)),
).fetchall()
out = []
for row in rows:
try:
out.append(json.loads(row["payload_json"]))
except Exception:
continue
return out
def load_rows_by_city_date(self, city: str, target_date: str) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT payload_json
FROM probability_training_snapshots_store
WHERE city = ? AND target_date = ?
ORDER BY timestamp ASC, id ASC
""",
(city, target_date),
).fetchall()
out = []
for row in rows:
try:
out.append(json.loads(row["payload_json"]))
except Exception:
continue
return out
def load_all_rows(self) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"SELECT payload_json FROM probability_training_snapshots_store ORDER BY id"
).fetchall()
out = []
for row in rows:
try:
out.append(json.loads(row["payload_json"]))
except Exception:
continue
return out
def replace_all(self, rows: List[Dict[str, Any]]) -> int:
count = 0
with self.db.connect() as conn:
conn.execute("DELETE FROM probability_training_snapshots_store")
for payload in rows or []:
if not isinstance(payload, dict):
continue
legacy_top = _top_bucket(payload.get("prob_snapshot"))
shadow_top = _top_bucket(payload.get("shadow_prob_snapshot"))
conn.execute(
"""
INSERT INTO probability_training_snapshots_store (
city, target_date, timestamp, raw_mu, raw_sigma, max_so_far,
peak_status, probability_mode, legacy_top_bucket, shadow_top_bucket, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payload.get("city"),
payload.get("date"),
payload.get("timestamp"),
payload.get("raw_mu"),
payload.get("raw_sigma"),
payload.get("max_so_far"),
payload.get("peak_status"),
payload.get("probability_mode"),
legacy_top,
shadow_top,
json.dumps(payload, ensure_ascii=False),
),
)
count += 1
conn.commit()
return count
class TrainingFeatureRecordRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def upsert_record(self, city: str, target_date: str, payload: Dict[str, Any]) -> None:
with self.db.connect() as conn:
conn.execute(
"""
INSERT INTO training_feature_records_store (
city, target_date, updated_at, payload_json
) VALUES (?, ?, ?, ?)
ON CONFLICT(city, target_date) DO UPDATE SET
updated_at = excluded.updated_at,
payload_json = excluded.payload_json
""",
(
city,
target_date,
time.time(),
json.dumps(payload, ensure_ascii=False),
),
)
conn.commit()
def load_all(self) -> Dict[str, Dict[str, Dict[str, Any]]]:
out: Dict[str, Dict[str, Dict[str, Any]]] = {}
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT city, target_date, payload_json
FROM training_feature_records_store
ORDER BY city, target_date
"""
).fetchall()
for row in rows:
try:
payload = json.loads(row["payload_json"])
except Exception:
continue
out.setdefault(str(row["city"]), {})[str(row["target_date"])] = payload
return out
def get_record(self, city: str, target_date: str) -> Optional[Dict[str, Any]]:
with self.db.connect() as conn:
row = conn.execute(
"""
SELECT payload_json
FROM training_feature_records_store
WHERE city = ? AND target_date = ?
""",
(city, target_date),
).fetchone()
if not row:
return None
try:
return json.loads(row["payload_json"])
except Exception:
return None
def load_city(self, city: str) -> Dict[str, Dict[str, Any]]:
out: Dict[str, Dict[str, Any]] = {}
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT target_date, payload_json
FROM training_feature_records_store
WHERE city = ?
ORDER BY target_date
""",
(city,),
).fetchall()
for row in rows:
try:
out[str(row["target_date"])] = json.loads(row["payload_json"])
except Exception:
continue
return out
class OpenMeteoCacheRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def replace_payload(self, payload: Dict[str, Any], max_age: int) -> int:
count = 0
now = time.time()
with self.db.connect() as conn:
conn.execute("DELETE FROM open_meteo_cache_store")
for source_kind in ("forecast", "ensemble", "multi_model"):
bucket = payload.get(source_kind) or {}
if not isinstance(bucket, dict):
continue
for cache_key, entry in bucket.items():
if not isinstance(entry, dict):
continue
updated_at = float(entry.get("t") or now)
expires_at = updated_at + max_age
conn.execute(
"""
INSERT INTO open_meteo_cache_store (
source_kind, cache_key, updated_at, expires_at, payload_json
) VALUES (?, ?, ?, ?, ?)
""",
(
source_kind,
cache_key,
updated_at,
expires_at,
json.dumps(entry, ensure_ascii=False),
),
)
count += 1
conn.commit()
return count
def load_payload(self, max_age: int) -> Dict[str, Any]:
now = time.time()
payload: Dict[str, Any] = {
"forecast": {},
"ensemble": {},
"multi_model": {},
"saved_at": now,
}
with self.db.connect() as conn:
rows = conn.execute(
"SELECT source_kind, cache_key, updated_at, payload_json FROM open_meteo_cache_store"
).fetchall()
for row in rows:
updated_at = float(row["updated_at"] or 0)
if now - updated_at >= max(600, max_age):
continue
try:
entry = json.loads(row["payload_json"])
except Exception:
continue
payload.setdefault(str(row["source_kind"]), {})[str(row["cache_key"])] = entry
return payload
def latest_updated_at(self) -> float:
with self.db.connect() as conn:
row = conn.execute(
"SELECT MAX(updated_at) AS max_updated_at FROM open_meteo_cache_store"
).fetchone()
if not row:
return 0.0
try:
return float(row["max_updated_at"] or 0.0)
except Exception:
return 0.0
class OfficialIntradayObservationRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def upsert_point(
self,
*,
source_code: str,
station_code: str,
target_date: str,
observation_time: str,
value: float,
payload: Optional[Dict[str, Any]] = None,
) -> None:
payload_json = json.dumps(payload, ensure_ascii=False) if payload is not None else None
with self.db.connect() as conn:
conn.execute(
"""
INSERT INTO official_intraday_observations_store (
source_code, station_code, target_date, observation_time, value, payload_json
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(source_code, station_code, observation_time) DO UPDATE SET
target_date = excluded.target_date,
value = excluded.value,
payload_json = excluded.payload_json
""",
(
source_code,
station_code,
target_date,
observation_time,
float(value),
payload_json,
),
)
conn.commit()
def load_points(
self,
*,
source_code: str,
station_code: str,
target_date: str,
) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT observation_time, value, payload_json
FROM official_intraday_observations_store
WHERE source_code = ? AND station_code = ? AND target_date = ?
ORDER BY observation_time
""",
(source_code, station_code, target_date),
).fetchall()
out: List[Dict[str, Any]] = []
for row in rows:
point = {
"time": str(row["observation_time"] or "").strip(),
"temp": float(row["value"]),
}
if row["payload_json"]:
try:
payload = json.loads(row["payload_json"])
except Exception:
payload = None
if isinstance(payload, dict):
point.update(payload)
if point["time"]:
out.append(point)
return out
class IntradayPathSnapshotRepository:
def __init__(self, db: Optional[RuntimeStateDB] = None):
self.db = db or RuntimeStateDB.instance()
def append_snapshot(self, payload: Dict[str, Any]) -> None:
observations = []
for key in ("metar_today_obs", "settlement_today_obs"):
rows = payload.get(key)
if isinstance(rows, list):
observations.extend(rows)
with self.db.connect() as conn:
conn.execute(
"""
INSERT INTO intraday_path_snapshots_store (
city, target_date, snapshot_time, local_time,
deb_prediction, forecast_today_high, current_temp,
max_so_far, observation_count, payload_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
payload.get("city"),
payload.get("target_date"),
payload.get("snapshot_time"),
payload.get("local_time"),
payload.get("deb_prediction"),
payload.get("forecast_today_high"),
payload.get("current_temp"),
payload.get("max_so_far"),
len(observations),
json.dumps(payload, ensure_ascii=False),
),
)
conn.commit()
def load_rows_by_city_date(self, city: str, target_date: str) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"""
SELECT payload_json
FROM intraday_path_snapshots_store
WHERE city = ? AND target_date = ?
ORDER BY id ASC
""",
(city, target_date),
).fetchall()
out: List[Dict[str, Any]] = []
for row in rows:
try:
out.append(json.loads(row["payload_json"]))
except Exception:
continue
return out
def load_all_rows(self) -> List[Dict[str, Any]]:
with self.db.connect() as conn:
rows = conn.execute(
"SELECT payload_json FROM intraday_path_snapshots_store ORDER BY id ASC"
).fetchall()
out: List[Dict[str, Any]] = []
for row in rows:
try:
out.append(json.loads(row["payload_json"]))
except Exception:
continue
return out
def _top_bucket(snapshot: Optional[List[Dict[str, Any]]]) -> Optional[int]:
best_value = None
best_prob = -1.0
for row in snapshot or []:
if not isinstance(row, dict):
continue
value = row.get("v")
if value is None:
value = row.get("value")
try:
ivalue = int(value)
except Exception:
continue
prob = row.get("p")
if prob is None:
prob = row.get("probability")
try:
fprob = float(prob)
except Exception:
continue
if fprob > best_prob:
best_prob = fprob
best_value = ivalue
return best_value
def get_runtime_data_dir() -> str:
raw = str(os.getenv("POLYWEATHER_RUNTIME_DATA_DIR") or "").strip()
if raw:
return raw
project_root = Path(__file__).resolve().parents[2]
return str(project_root / "data")