修复 analysis_service.py 语法错误 + scan.py Python 3.8 兼容

This commit is contained in:
2569718930@qq.com
2026-05-25 17:54:44 +08:00
parent 617b55f24c
commit c7cfd2cb59
+109 -11
View File
@@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
import re
import time as _time import time as _time
import threading as _threading import threading
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
@@ -42,15 +43,8 @@ from web.services.analysis_utils import (
add_signal as _add_signal, add_signal as _add_signal,
bucket_label as _bucket_label, bucket_label as _bucket_label,
bucket_label_from_value as _bucket_label_from_value, bucket_label_from_value as _bucket_label_from_value,
dedupe_forecast_daily as _dedupe_forecast_daily,
format_clock_minutes as _format_clock_minutes, format_clock_minutes as _format_clock_minutes,
format_observation_time_local as _format_observation_time_local,
is_plausible_city_temp as _is_plausible_city_temp,
metar_is_current_local_day as _metar_is_current_local_day,
mgm_hourly_high as _mgm_hourly_high,
next_observation_clock as _next_observation_clock, next_observation_clock as _next_observation_clock,
parse_local_hour as _parse_local_hour,
parse_utc_datetime as _parse_utc_datetime,
top_probability_bucket as _top_probability_bucket, top_probability_bucket as _top_probability_bucket,
) )
from web.services.analysis_signals import ( from web.services.analysis_signals import (
@@ -85,7 +79,20 @@ HIGH_FREQ_AIRPORT_ANALYSIS_CITIES = {
"wuhan", "wuhan",
} }
_ANALYSIS_CACHE_STATS_LOCK = _threading.Lock()
def _mgm_hourly_high(mgm: Dict[str, Any]) -> Optional[float]:
hourly = mgm.get("hourly") if isinstance(mgm, dict) else []
if not isinstance(hourly, list):
return None
values = []
for row in hourly:
if not isinstance(row, dict):
continue
value = _sf(row.get("temp"))
if value is not None:
values.append(value)
return max(values) if values else None
_ANALYSIS_CACHE_STATS_LOCK = threading.Lock()
_ANALYSIS_CACHE_STATS: Dict[str, Any] = { _ANALYSIS_CACHE_STATS: Dict[str, Any] = {
"total_requests": 0, "total_requests": 0,
"cache_hits": 0, "cache_hits": 0,
@@ -95,14 +102,105 @@ _ANALYSIS_CACHE_STATS: Dict[str, Any] = {
"last_cache_miss_at": None, "last_cache_miss_at": None,
"last_city": None, "last_city": None,
} }
_SUMMARY_CACHE_LOCK = _threading.Lock() _SUMMARY_CACHE_LOCK = threading.Lock()
_SUMMARY_CACHE = LRUDict(maxsize=128) _SUMMARY_CACHE_MAXSIZE = 128
_SUMMARY_CACHE = LRUDict(maxsize=_SUMMARY_CACHE_MAXSIZE)
def _dedupe_forecast_daily(rows: Any) -> list[Dict[str, Any]]:
if not isinstance(rows, list):
return []
seen = set()
out = []
for row in rows:
if not isinstance(row, dict):
continue
date = str(row.get("date") or "").strip()
if not date or date in seen:
continue
seen.add(date)
out.append(row)
return out
def _format_observation_time_local(value: Any, utc_offset: int) -> str:
raw = str(value or "").strip()
if not raw:
return ""
if "T" in raw:
try:
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone(timedelta(seconds=utc_offset))).strftime("%H:%M")
except Exception:
pass
match = re.search(r"(\d{1,2}):(\d{2})", raw)
if match:
return f"{int(match.group(1)):02d}:{match.group(2)}"
return raw[:16]
def _fetch_nmc_current_fallback(city: str, *, use_fahrenheit: bool) -> Dict[str, Any]: def _fetch_nmc_current_fallback(city: str, *, use_fahrenheit: bool) -> Dict[str, Any]:
return {} return {}
def _is_plausible_city_temp(city: str, value: Any, unit: str = "°C") -> bool:
temp = _sf(value)
if temp is None:
return False
meta = CITY_REGISTRY.get(str(city or "").strip().lower(), {}) or {}
min_c = _sf(meta.get("min_plausible_metar_temp_c"))
if min_c is None:
return True
min_value = min_c * 9 / 5 + 32 if str(unit or "").upper().endswith("F") else min_c
return temp >= min_value
def _parse_local_hour(local_time_str: Optional[str]) -> Optional[int]:
if not local_time_str:
return None
try:
parts = str(local_time_str).strip().split(":")
hour = int(parts[0])
if 0 <= hour <= 23:
return hour
except Exception:
pass
return None
def _parse_utc_datetime(value: Any) -> Optional[datetime]:
raw = str(value or "").strip()
if not raw or "T" not in raw:
return None
try:
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def _metar_is_current_local_day(
metar: Dict[str, Any],
*,
local_date: str,
utc_offset: int,
) -> bool:
if not isinstance(metar, dict) or not metar:
return False
if metar.get("stale_for_today") is True:
return False
observation_local_date = str(metar.get("observation_local_date") or "").strip()
if observation_local_date:
return observation_local_date == local_date
obs_dt = _parse_utc_datetime(metar.get("observation_time"))
if obs_dt is None:
return True
local_dt = obs_dt.astimezone(timezone(timedelta(seconds=utc_offset)))
return local_dt.strftime("%Y-%m-%d") == local_date
def _record_analysis_cache_event(*, city: str, hit: bool, force_refresh: bool) -> None: def _record_analysis_cache_event(*, city: str, hit: bool, force_refresh: bool) -> None:
now = datetime.now(timezone.utc).isoformat() now = datetime.now(timezone.utc).isoformat()
with _ANALYSIS_CACHE_STATS_LOCK: with _ANALYSIS_CACHE_STATS_LOCK: