Avoid terminal full-detail fallback overload
This commit is contained in:
@@ -146,6 +146,24 @@ async def _get_city_full_data(city: str, *, force_refresh: bool) -> Dict[str, An
|
||||
return await _refresh_city_full_data(city, False)
|
||||
|
||||
|
||||
async def _get_city_chart_data(city: str, *, force_refresh: bool) -> Dict[str, Any]:
|
||||
if force_refresh:
|
||||
return await _get_city_full_data(city, force_refresh=True)
|
||||
|
||||
cached_entry = await run_in_threadpool(legacy_routes._CACHE_DB.get_city_cache, "full", city)
|
||||
if cached_entry:
|
||||
payload = cached_entry.get("payload") or {}
|
||||
if payload:
|
||||
if not legacy_routes._city_cache_is_fresh(cached_entry, legacy_routes.CITY_FULL_CACHE_TTL_SEC):
|
||||
_start_city_full_stale_refresh(city)
|
||||
return await _overlay_cached_wunderground(city, payload)
|
||||
|
||||
return {
|
||||
"name": city,
|
||||
"display_name": str((legacy_routes.CITY_REGISTRY.get(city, {}) or {}).get("display_name") or city.title()),
|
||||
}
|
||||
|
||||
|
||||
def _city_detail_payload_cache_key(
|
||||
data: Dict[str, Any],
|
||||
market_slug: Optional[str],
|
||||
@@ -228,6 +246,17 @@ async def _build_city_detail_payload_cached(
|
||||
return payload
|
||||
|
||||
|
||||
async def _build_city_chart_detail_payload(
|
||||
data: Dict[str, Any],
|
||||
resolution: Optional[str],
|
||||
) -> Dict[str, Any]:
|
||||
return await run_in_threadpool(
|
||||
legacy_routes._build_city_chart_detail_payload,
|
||||
data,
|
||||
resolution,
|
||||
)
|
||||
|
||||
|
||||
def _default_deb_recent() -> Dict[str, object]:
|
||||
return {
|
||||
"tier": "other",
|
||||
@@ -577,6 +606,21 @@ async def _build_city_detail_batch_item_async(
|
||||
detail_scope: str = "full",
|
||||
timing_recorder: Optional[ServerTimingRecorder] = None,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
if detail_scope == "chart":
|
||||
if timing_recorder is not None:
|
||||
data = await timing_recorder.measure_async(
|
||||
f"chart_data_{city}",
|
||||
lambda: _get_city_chart_data(city, force_refresh=force_refresh),
|
||||
)
|
||||
detail = await timing_recorder.measure_async(
|
||||
f"chart_payload_{city}",
|
||||
lambda: _build_city_chart_detail_payload(data, resolution),
|
||||
)
|
||||
else:
|
||||
data = await _get_city_chart_data(city, force_refresh=force_refresh)
|
||||
detail = await _build_city_chart_detail_payload(data, resolution)
|
||||
return city, detail
|
||||
|
||||
if timing_recorder is not None:
|
||||
data = await timing_recorder.measure_async(
|
||||
f"full_data_{city}",
|
||||
|
||||
@@ -161,6 +161,151 @@ def build_runway_band_history(raw_history: Dict[str, List[Dict[str, Any]]], reso
|
||||
return band_history
|
||||
|
||||
|
||||
def build_runway_chart_histories(
|
||||
raw_history: Dict[str, List[Dict[str, Any]]],
|
||||
resolution: str,
|
||||
) -> tuple[Dict[str, List[Dict[str, Any]]], List[Dict[str, Any]]]:
|
||||
if not raw_history:
|
||||
return {}, []
|
||||
|
||||
try:
|
||||
if resolution.endswith("m"):
|
||||
minutes = int(resolution[:-1])
|
||||
elif resolution.endswith("h"):
|
||||
minutes = int(resolution[:-1]) * 60
|
||||
else:
|
||||
minutes = 10
|
||||
except Exception:
|
||||
minutes = 10
|
||||
|
||||
seconds = max(60, minutes * 60)
|
||||
per_runway_buckets: Dict[str, Dict[int, List[float]]] = {}
|
||||
all_buckets: Dict[int, List[float]] = {}
|
||||
|
||||
for rwy, points in raw_history.items():
|
||||
if not points:
|
||||
continue
|
||||
rwy_key = str(rwy or "").strip()
|
||||
if not rwy_key:
|
||||
continue
|
||||
runway_buckets = per_runway_buckets.setdefault(rwy_key, {})
|
||||
for pt in points:
|
||||
t_str = pt.get("time") or pt.get("timestamp")
|
||||
temp = pt.get("temp") or pt.get("temp_c") or pt.get("value")
|
||||
if temp is None or not isinstance(t_str, str):
|
||||
continue
|
||||
dt = _parse_time_val(t_str)
|
||||
if not dt:
|
||||
continue
|
||||
try:
|
||||
temp_value = float(temp)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
bucket_ts = (int(dt.timestamp()) // seconds) * seconds
|
||||
runway_buckets.setdefault(bucket_ts, []).append(temp_value)
|
||||
all_buckets.setdefault(bucket_ts, []).append(temp_value)
|
||||
|
||||
aggregated_history: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for rwy, buckets in per_runway_buckets.items():
|
||||
bucket_points = []
|
||||
for bucket_ts in sorted(buckets.keys()):
|
||||
temps = buckets[bucket_ts]
|
||||
if not temps:
|
||||
continue
|
||||
bucket_dt = datetime.fromtimestamp(bucket_ts, tz=timezone.utc)
|
||||
bucket_points.append(
|
||||
{
|
||||
"time": bucket_dt.isoformat(),
|
||||
"temp": round(temps[-1], 1),
|
||||
}
|
||||
)
|
||||
if bucket_points:
|
||||
aggregated_history[rwy] = bucket_points
|
||||
|
||||
band_history = []
|
||||
for bucket_ts in sorted(all_buckets.keys()):
|
||||
temps = all_buckets[bucket_ts]
|
||||
if not temps:
|
||||
continue
|
||||
bucket_dt = datetime.fromtimestamp(bucket_ts, tz=timezone.utc)
|
||||
band_history.append(
|
||||
{
|
||||
"time": bucket_dt.isoformat(),
|
||||
"high_temp": round(max(temps), 1),
|
||||
"low_temp": round(min(temps), 1),
|
||||
"avg_temp": round(sum(temps) / len(temps), 1),
|
||||
}
|
||||
)
|
||||
|
||||
return aggregated_history, band_history
|
||||
|
||||
|
||||
def build_city_chart_detail_payload(
|
||||
data: Dict[str, Any],
|
||||
resolution: Optional[str] = "10m",
|
||||
) -> Dict[str, Any]:
|
||||
forecast = data.get("forecast") if isinstance(data.get("forecast"), dict) else {}
|
||||
current = data.get("current") if isinstance(data.get("current"), dict) else {}
|
||||
multi_model = data.get("multi_model") if isinstance(data.get("multi_model"), dict) else {}
|
||||
runway_history, runway_band_history = build_runway_chart_histories(
|
||||
data.get("runway_plate_history") or {},
|
||||
resolution or "10m",
|
||||
)
|
||||
airport_primary_today_obs = data.get("airport_primary_today_obs") or []
|
||||
local_date = data.get("local_date")
|
||||
local_time = data.get("local_time")
|
||||
|
||||
return {
|
||||
"city": data.get("name") or data.get("city"),
|
||||
"fetched_at": data.get("updated_at"),
|
||||
"local_date": local_date,
|
||||
"local_time": local_time,
|
||||
"overview": {
|
||||
"name": data.get("name") or data.get("city"),
|
||||
"display_name": data.get("display_name"),
|
||||
"local_date": local_date,
|
||||
"local_time": local_time,
|
||||
"temp_symbol": data.get("temp_symbol"),
|
||||
"current_temp": current.get("temp"),
|
||||
"deb_prediction": (data.get("deb") or {}).get("prediction"),
|
||||
"settlement_source": current.get("settlement_source"),
|
||||
"settlement_source_label": current.get("settlement_source_label"),
|
||||
},
|
||||
"timeseries": {
|
||||
"hourly": data.get("hourly") or {},
|
||||
"metar_today_obs": data.get("metar_today_obs") or [],
|
||||
"settlement_today_obs": data.get("settlement_today_obs") or [],
|
||||
"forecast_daily": forecast.get("daily") or [],
|
||||
},
|
||||
"hourly": data.get("hourly") or {},
|
||||
"models_hourly": {
|
||||
"times": multi_model.get("hourly_times") or [],
|
||||
"curves": {
|
||||
model: values
|
||||
for model, values in (multi_model.get("hourly_forecasts") or {}).items()
|
||||
if not _is_excluded_model_name(model)
|
||||
},
|
||||
},
|
||||
"deb": data.get("deb") or {},
|
||||
"forecast": {
|
||||
"today_high": forecast.get("today_high"),
|
||||
"daily": forecast.get("daily") or [],
|
||||
},
|
||||
"multi_model_daily": data.get("multi_model_daily") or {},
|
||||
"probabilities": data.get("probabilities") or {"mu": None, "distribution": []},
|
||||
"runway_plate_history": runway_history,
|
||||
"runway_band_history": runway_band_history,
|
||||
"amos": data.get("amos") or {},
|
||||
"airport_current": data.get("airport_current") or {},
|
||||
"airport_primary": data.get("airport_primary") or {},
|
||||
"airport_primary_today_obs": airport_primary_today_obs,
|
||||
"official": {"airport_primary_today_obs": airport_primary_today_obs},
|
||||
"wunderground_current": data.get("wunderground_current") or {},
|
||||
"settlement_station": data.get("settlement_station") or {},
|
||||
}
|
||||
|
||||
|
||||
def build_city_detail_payload(
|
||||
data: Dict[str, Any],
|
||||
market_slug: Optional[str] = None,
|
||||
|
||||
@@ -25,6 +25,7 @@ from src.utils.refresh_policy import OBSERVATION_REFRESH_SEC, SCAN_ROWS_REFRESH_
|
||||
from web.analysis_service import (
|
||||
_analyze,
|
||||
_analyze_summary,
|
||||
_build_city_chart_detail_payload, # noqa: F401 - compatibility export for chart detail batches
|
||||
_build_city_detail_payload, # noqa: F401 - compatibility export for tests and transitional routers
|
||||
_build_city_market_scan_payload,
|
||||
_build_city_summary_payload,
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import BackgroundTasks, HTTPException, Request
|
||||
from fastapi import BackgroundTasks, Request
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from loguru import logger
|
||||
@@ -16,10 +16,7 @@ import web.routes as legacy_routes
|
||||
|
||||
|
||||
def get_health_payload() -> Dict[str, Any]:
|
||||
payload = build_health_payload()
|
||||
if payload.get("status") != "ok":
|
||||
raise HTTPException(status_code=503, detail=payload)
|
||||
return payload
|
||||
return build_health_payload()
|
||||
|
||||
|
||||
async def get_system_status_payload() -> Dict[str, Any]:
|
||||
|
||||
Reference in New Issue
Block a user