Make city history read from SQLite truth and feature records

This commit is contained in:
2569718930@qq.com
2026-04-05 10:57:51 +08:00
parent 92a1a7de12
commit e2c43351bd
4 changed files with 127 additions and 14 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "PolyWeather Side Panel",
"description": "Weather side panel for Polymarket.",
"version": "0.1.8",
"version": "0.1.9",
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
+52
View File
@@ -360,6 +360,39 @@ class TruthRecordRepository:
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,
*,
@@ -789,6 +822,25 @@ class TrainingFeatureRecordRepository:
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):
+45 -1
View File
@@ -3,7 +3,7 @@ from fastapi.testclient import TestClient
from web.app import app
import web.routes as routes
from src.database.runtime_state import TruthRecordRepository
from src.database.runtime_state import TruthRecordRepository, TrainingFeatureRecordRepository
client = TestClient(app)
@@ -200,3 +200,47 @@ def test_ops_truth_history_returns_filtered_rows(monkeypatch):
assert payload["filters"]["city"] == "taipei"
assert payload["items"][0]["city"] == "taipei"
assert payload["items"][0]["settlement_station_code"] == "RCSS"
def test_city_history_is_read_only_and_uses_sqlite_truth_and_features(monkeypatch):
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
def _fail_bootstrap(*args, **kwargs):
raise AssertionError("bootstrap should not be called by /api/history")
monkeypatch.setattr(routes, "bootstrap_recent_daily_history_if_missing", _fail_bootstrap, raising=False)
truth_repo = TruthRecordRepository()
truth_repo.upsert_truth(
city="ankara",
target_date="2026-04-02",
actual_high=16.0,
settlement_source="metar",
settlement_station_code="LTAC",
settlement_station_label="Ankara Esenboga Airport",
truth_version="v1",
updated_by="test",
source_payload={"sample": True},
is_final=True,
)
feature_repo = TrainingFeatureRecordRepository()
feature_repo.upsert_record(
"ankara",
"2026-04-02",
{
"deb_prediction": 16.4,
"mu": 16.2,
"forecasts": {"Open-Meteo": 15.8, "ECMWF": 16.1},
},
)
response = client.get("/api/history/ankara")
assert response.status_code == 200
payload = response.json()
assert payload["history"]
row = next(item for item in payload["history"] if item["date"] == "2026-04-02")
assert row["actual"] == 16.0
assert row["deb"] == 16.4
assert row["mu"] == 16.2
assert row["forecasts"]["Open-Meteo"] == 15.8
+29 -12
View File
@@ -8,11 +8,9 @@ from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import PlainTextResponse
from loguru import logger
from src.analysis.deb_algorithm import (
bootstrap_recent_daily_history_if_missing,
load_history,
)
from src.analysis.deb_algorithm import load_history
from src.analysis.probability_snapshot_archive import load_snapshot_rows_for_day
from src.database.runtime_state import TrainingFeatureRecordRepository, TruthRecordRepository
from src.analysis.settlement_rounding import apply_city_settlement
from src.data_collection.city_registry import ALIASES
from src.database.runtime_state import TruthRecordRepository
@@ -56,6 +54,8 @@ router = APIRouter()
_DEB_RECENT_LOOKBACK = 7
_DEB_RECENT_MIN_SAMPLES = 3
_truth_record_repo = TruthRecordRepository()
_training_feature_repo = TrainingFeatureRecordRepository()
TRACKABLE_ANALYTICS_EVENTS = {
"signup_completed",
@@ -312,16 +312,34 @@ async def city_detail(request: Request, name: str, force_refresh: bool = False):
async def city_history(request: Request, name: str):
_assert_entitlement(request)
city = _normalize_city_or_404(name)
source = str(CITIES.get(city, {}).get("settlement_source") or "metar").strip().lower()
truth_rows = _truth_record_repo.load_city(city)
feature_rows = _training_feature_repo.load_city(city)
bootstrap_recent_daily_history_if_missing(city, lookback_days=14)
if not truth_rows and not feature_rows:
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
history_file = os.path.join(project_root, "data", "daily_records.json")
data = load_history(history_file)
city_data = data.get(city, {}) if isinstance(data.get(city, {}), dict) else {}
else:
all_dates = sorted(set(truth_rows.keys()) | set(feature_rows.keys()))
city_data = {}
for day in all_dates:
record: dict[str, object] = {}
truth = truth_rows.get(day) or {}
features = feature_rows.get(day) or {}
if truth.get("actual_high") is not None:
record["actual_high"] = truth.get("actual_high")
if isinstance(features, dict):
if features.get("deb_prediction") is not None:
record["deb_prediction"] = features.get("deb_prediction")
if features.get("mu") is not None:
record["mu"] = features.get("mu")
if isinstance(features.get("forecasts"), dict):
record["forecasts"] = features.get("forecasts")
city_data[day] = record
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
history_file = os.path.join(project_root, "data", "daily_records.json")
data = load_history(history_file)
city_data = data.get(city, {}) if isinstance(data.get(city, {}), dict) else {}
if not city_data:
source = str(CITIES.get(city, {}).get("settlement_source") or "metar").strip().lower()
return {
"history": [],
"settlement_source": source,
@@ -369,7 +387,6 @@ async def city_history(request: Request, name: str):
}
)
source = str(CITIES.get(city, {}).get("settlement_source") or "metar").strip().lower()
return {
"history": out,
"settlement_source": source,