Make city history read from SQLite truth and feature records
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user