Make city history read from SQLite truth and feature records
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "PolyWeather Side Panel",
|
"name": "PolyWeather Side Panel",
|
||||||
"description": "Weather side panel for Polymarket.",
|
"description": "Weather side panel for Polymarket.",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icon-16.png",
|
"16": "icon-16.png",
|
||||||
"32": "icon-32.png",
|
"32": "icon-32.png",
|
||||||
|
|||||||
@@ -360,6 +360,39 @@ class TruthRecordRepository:
|
|||||||
pass
|
pass
|
||||||
return payload
|
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(
|
def upsert_truth(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -789,6 +822,25 @@ class TrainingFeatureRecordRepository:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
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:
|
class OpenMeteoCacheRepository:
|
||||||
def __init__(self, db: Optional[RuntimeStateDB] = None):
|
def __init__(self, db: Optional[RuntimeStateDB] = None):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
from web.app import app
|
from web.app import app
|
||||||
import web.routes as routes
|
import web.routes as routes
|
||||||
from src.database.runtime_state import TruthRecordRepository
|
from src.database.runtime_state import TruthRecordRepository, TrainingFeatureRecordRepository
|
||||||
|
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
@@ -200,3 +200,47 @@ def test_ops_truth_history_returns_filtered_rows(monkeypatch):
|
|||||||
assert payload["filters"]["city"] == "taipei"
|
assert payload["filters"]["city"] == "taipei"
|
||||||
assert payload["items"][0]["city"] == "taipei"
|
assert payload["items"][0]["city"] == "taipei"
|
||||||
assert payload["items"][0]["settlement_station_code"] == "RCSS"
|
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 fastapi.responses import PlainTextResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.analysis.deb_algorithm import (
|
from src.analysis.deb_algorithm import load_history
|
||||||
bootstrap_recent_daily_history_if_missing,
|
|
||||||
load_history,
|
|
||||||
)
|
|
||||||
from src.analysis.probability_snapshot_archive import load_snapshot_rows_for_day
|
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.analysis.settlement_rounding import apply_city_settlement
|
||||||
from src.data_collection.city_registry import ALIASES
|
from src.data_collection.city_registry import ALIASES
|
||||||
from src.database.runtime_state import TruthRecordRepository
|
from src.database.runtime_state import TruthRecordRepository
|
||||||
@@ -56,6 +54,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
_DEB_RECENT_LOOKBACK = 7
|
_DEB_RECENT_LOOKBACK = 7
|
||||||
_DEB_RECENT_MIN_SAMPLES = 3
|
_DEB_RECENT_MIN_SAMPLES = 3
|
||||||
|
_truth_record_repo = TruthRecordRepository()
|
||||||
|
_training_feature_repo = TrainingFeatureRecordRepository()
|
||||||
|
|
||||||
TRACKABLE_ANALYTICS_EVENTS = {
|
TRACKABLE_ANALYTICS_EVENTS = {
|
||||||
"signup_completed",
|
"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):
|
async def city_history(request: Request, name: str):
|
||||||
_assert_entitlement(request)
|
_assert_entitlement(request)
|
||||||
city = _normalize_city_or_404(name)
|
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:
|
if not city_data:
|
||||||
source = str(CITIES.get(city, {}).get("settlement_source") or "metar").strip().lower()
|
|
||||||
return {
|
return {
|
||||||
"history": [],
|
"history": [],
|
||||||
"settlement_source": source,
|
"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 {
|
return {
|
||||||
"history": out,
|
"history": out,
|
||||||
"settlement_source": source,
|
"settlement_source": source,
|
||||||
|
|||||||
Reference in New Issue
Block a user