拆分 analysis_service:时钟工具和概率桶函数移至 analysis_utils.py
抽取 clock_minutes/format_clock_minutes/next_observation_clock 和 bucket_label/top_probability_bucket/add_signal 到独立模块。analysis_service 2152→2080 行。
This commit is contained in:
+8
-78
@@ -39,6 +39,14 @@ from web.services.observation_freshness import (
|
||||
build_observation_freshness as _build_observation_freshness,
|
||||
observation_age_min as _observation_age_min,
|
||||
)
|
||||
from web.services.analysis_utils import (
|
||||
add_signal as _add_signal,
|
||||
bucket_label as _bucket_label,
|
||||
bucket_label_from_value as _bucket_label_from_value,
|
||||
format_clock_minutes as _format_clock_minutes,
|
||||
next_observation_clock as _next_observation_clock,
|
||||
top_probability_bucket as _top_probability_bucket,
|
||||
)
|
||||
from web.services.analysis_signals import (
|
||||
_build_deviation_monitor,
|
||||
_build_taf_signal,
|
||||
@@ -305,84 +313,6 @@ def _maybe_enrich_dynamic_commentary_with_groq(
|
||||
|
||||
|
||||
|
||||
|
||||
def _clock_minutes(value: Any) -> Optional[int]:
|
||||
text = str(value or "").strip()
|
||||
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
|
||||
if not match:
|
||||
return None
|
||||
hour = int(match.group(1))
|
||||
minute = int(match.group(2))
|
||||
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
||||
return None
|
||||
return hour * 60 + minute
|
||||
|
||||
|
||||
def _format_clock_minutes(value: int) -> str:
|
||||
value = max(0, min(23 * 60 + 59, int(value)))
|
||||
return f"{value // 60:02d}:{value % 60:02d}"
|
||||
|
||||
|
||||
def _next_observation_clock(local_time: Any) -> str:
|
||||
minutes = _clock_minutes(local_time)
|
||||
if minutes is None:
|
||||
return "--"
|
||||
next_slot = ((minutes // 30) + 1) * 30
|
||||
if next_slot > 23 * 60 + 59:
|
||||
return "23:59"
|
||||
return _format_clock_minutes(next_slot)
|
||||
|
||||
|
||||
def _bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return f"{int(round(float(value)))}{unit or '°C'}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(distribution, list):
|
||||
return None
|
||||
candidates = [row for row in distribution if isinstance(row, dict)]
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
|
||||
|
||||
|
||||
def _bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for key in ("label", "bucket", "range"):
|
||||
raw = str(row.get(key) or "").strip()
|
||||
if raw:
|
||||
return raw
|
||||
return _bucket_label_from_value(_sf(row.get("value")), unit)
|
||||
|
||||
|
||||
def _add_signal(
|
||||
signals: list,
|
||||
*,
|
||||
label: str,
|
||||
direction: str,
|
||||
strength: str,
|
||||
summary: str,
|
||||
label_en: Optional[str] = None,
|
||||
summary_en: Optional[str] = None,
|
||||
) -> None:
|
||||
signals.append(
|
||||
{
|
||||
"label": label,
|
||||
"label_en": label_en or label,
|
||||
"direction": direction,
|
||||
"strength": strength,
|
||||
"summary": summary,
|
||||
"summary_en": summary_en or summary,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Build a paid-product intraday meteorology read from existing layers."""
|
||||
current = data.get("current") or {}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Analysis utility functions extracted from analysis_service.py.
|
||||
|
||||
Pure helpers: clock arithmetic, bucket labelling, signal packaging.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from web.core import _sf
|
||||
|
||||
|
||||
# ── Clock / time-slot helpers ──────────────────────────────────────────
|
||||
|
||||
def clock_minutes(value: Any) -> Optional[int]:
|
||||
text = str(value or "").strip()
|
||||
match = re.search(r"\b(\d{1,2}):(\d{2})\b", text)
|
||||
if not match:
|
||||
return None
|
||||
hour = int(match.group(1))
|
||||
minute = int(match.group(2))
|
||||
if hour < 0 or hour > 23 or minute < 0 or minute > 59:
|
||||
return None
|
||||
return hour * 60 + minute
|
||||
|
||||
|
||||
def format_clock_minutes(value: int) -> str:
|
||||
value = max(0, min(23 * 60 + 59, int(value)))
|
||||
return f"{value // 60:02d}:{value % 60:02d}"
|
||||
|
||||
|
||||
def next_observation_clock(local_time: Any) -> str:
|
||||
minutes = clock_minutes(local_time)
|
||||
if minutes is None:
|
||||
return "--"
|
||||
next_slot = ((minutes // 30) + 1) * 30
|
||||
if next_slot > 23 * 60 + 59:
|
||||
return "23:59"
|
||||
return format_clock_minutes(next_slot)
|
||||
|
||||
|
||||
# ── Probability bucket helpers ─────────────────────────────────────────
|
||||
|
||||
def bucket_label_from_value(value: Optional[float], unit: str) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return f"{int(round(float(value)))}{unit or '°C'}"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def top_probability_bucket(distribution: Any) -> Optional[Dict[str, Any]]:
|
||||
if not isinstance(distribution, list):
|
||||
return None
|
||||
candidates = [row for row in distribution if isinstance(row, dict)]
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda row: _sf(row.get("probability")) or -1.0)
|
||||
|
||||
|
||||
def bucket_label(row: Optional[Dict[str, Any]], unit: str) -> Optional[str]:
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
for key in ("label", "bucket", "range"):
|
||||
raw = str(row.get(key) or "").strip()
|
||||
if raw:
|
||||
return raw
|
||||
return bucket_label_from_value(_sf(row.get("value")), unit)
|
||||
|
||||
|
||||
# ── Signal packaging ───────────────────────────────────────────────────
|
||||
|
||||
def add_signal(
|
||||
signals: list,
|
||||
*,
|
||||
label: str,
|
||||
direction: str,
|
||||
strength: str,
|
||||
summary: str,
|
||||
label_en: Optional[str] = None,
|
||||
summary_en: Optional[str] = None,
|
||||
) -> None:
|
||||
signals.append(
|
||||
{
|
||||
"label": label,
|
||||
"label_en": label_en or label,
|
||||
"direction": direction,
|
||||
"strength": strength,
|
||||
"summary": summary,
|
||||
"summary_en": summary_en or summary,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user